commit e06e76e46bdfb9cda5041a6256815dc968500b06 Author: Zachary Levy Date: Sun Mar 9 12:13:14 2025 -0700 Initial proof of concept diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6175e3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# ---> Rust +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# IntelliJ +/.idea/ +out/ + +# CMake +cmake-build-*/ + +# File-based project format +*.iws + +*.stderr \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5eb80f7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,156 @@ +#--------------------------------------------------------------------------------------------------------------------- +#----- Workspace ------------------------ +#--------------------------------------------------------------------------------------------------------------------- +[workspace] +members = [ + # Device types + "node", + "commander", + # Drivers + "drivers/ads1256/types", + "drivers/ads1256/driver", + # Meta + "generate-quantity", + # Examples + "examples/ads1256" +] + +[workspace.package] +version = "0.4.6" +edition = "2021" +repository = "https://git.bfpower.io/BFPOWER/physical" +readme = "README.md" +license = "MIT" + +#----- no-std ---------------------------------- +# Numbers +[workspace.dependencies.num-traits] +version = "0.2.*" +default-features = false +[workspace.dependencies.libm] +version = "0.2.*" +[workspace.dependencies.float-cmp] +version = "0.9.*" +# Logging +[workspace.dependencies.tracing] +version = "0.1.*" +[workspace.dependencies.defmt] +version = "0.3.*" +[workspace.dependencies.defmt-rtt] +version = "0.4.*" +# Embedded-HAL +[workspace.dependencies.embedded-hal] +version = "1.0.*" +[workspace.dependencies.embedded-hal-async] +version = "1.0.*" +# Memory +[workspace.dependencies.static_cell] +version = "2.1.*" +# Serioalization +[workspace.dependencies.serde] +version = "1.0.*" +default-features = false +features = ["derive"] +# Other embedded utilities +[workspace.dependencies.cortex-m] +version = "0.7.*" +[workspace.dependencies.cortex-m-rt] +version = "0.7.*" +[workspace.dependencies.panic-probe] +version = "0.3.*" +features = ["print-defmt"] +# Embassy +[workspace.dependencies.embassy-futures] +version = "0.1.*" +[workspace.dependencies.embassy-time] +version = "0.3.*" +features = ["defmt", "defmt-timestamp-uptime"] +[workspace.dependencies.embassy-sync] +version = "0.6.*" +features = ["defmt"] +[workspace.dependencies.embassy-embedded-hal] +version = "0.1.*" +[workspace.dependencies.embassy-executor] +version = "0.5.*" +features = ["defmt", "arch-cortex-m", "integrated-timers", "executor-interrupt", "executor-thread"] +[workspace.dependencies.embassy-usb] +version = "0.2.*" +features = ["defmt"] +[workspace.dependencies.embassy-stm32] +version = "0.1.*" +features = ["defmt", "unstable-pac"] +[workspace.dependencies.embassy-nrf] +version = "0.1.*" +features = ["defmt"] +# Meta +[workspace.dependencies.derive_more] +version = "0.99.*" +[workspace.dependencies.syn] +version = "2.0.*" +features = ["extra-traits", "parsing"] +[workspace.dependencies.quote] +version = "1.0.*" +[workspace.dependencies.proc-macro2] +version = "1.0.*" +[workspace.dependencies.trybuild] +version = "1.0.*" + +#--------------------------------------------------------------------------------------------------------------------- +#----- Package ------------------------ +#--------------------------------------------------------------------------------------------------------------------- +[package] +name = "physical" +description = "Physical is a library for interacting with the physical world." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[features] +std = ["num-traits/std"] +libm = ["dep:libm", "num-traits/libm"] +resistive-divider = [] +thermocouple-k = ["libm"] +thermistor = ["libm"] +lm35 = [] +pid = [] +stm32 = [] + +[dependencies.generate-quantity] +path = "generate-quantity" +[dependencies.num-traits] +workspace = true +[dependencies.derive_more] +workspace = true +[dependencies.defmt] +workspace = true +optional = true +[dependencies.libm] +workspace = true +optional = true +[dependencies.serde] +workspace = true +optional = true + +[dev-dependencies.float-cmp] +workspace = true + +#--------------------------------------------------------------------------------------------------------------------- +#----- Profiles ------------------------ +#--------------------------------------------------------------------------------------------------------------------- +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" + +[profile.dev] +opt-level = 3 +debug = true +debug-assertions = true +overflow-checks = true +lto = true +panic = "abort" +incremental = false +codegen-units = 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2071b23 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c59a00 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Physical + +Physical is a library for interacting with the physical world from a computer. This can broadly be broken down into two +categories: + +* Collecting and digitizing data from the physical world. +* Controlling devices that take physical action. + +## Concepts + +The main concepts of Physical are: + +* Peripheral: A peripheral is a board that hosts physical I/O and usually does analog to digital conversion or + digital to analog conversion. A peripheral cannot function on its own, it must be connected to a node. This is more + narrow than the definition of a peripheral in embedded systems generally. Peripheral support is focused on complete + boards, not individual components like an ADC. Although common abstractions for components specific to physical + can be provided in this repository, general component abstractions / drivers should be made separately, such as in + BFPOWER drivers. +* Node: A node hosts peripherals. A node can have a commander but does not need one. A node can ignore or even override + commands from the commander. In a complex system, nodes are intended to be kept simple, less likely to + encounter an error than the commander, and in some cases should check for obvious problems in commands from the + commander. Node can also communicate with other nodes. +* Commander: A commander hosts nodes. It performs long running computations and directs nodes based on the results. \ No newline at end of file diff --git a/commander/Cargo.toml b/commander/Cargo.toml new file mode 100644 index 0000000..70fdefb --- /dev/null +++ b/commander/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "physical-commander" +description = "A commander hosts nodes." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies.physical] +path = ".." +[dependencies.tracing] +workspace = true \ No newline at end of file diff --git a/commander/src/lib.rs b/commander/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/drivers/ads1256/driver/Cargo.toml b/drivers/ads1256/driver/Cargo.toml new file mode 100644 index 0000000..1229462 --- /dev/null +++ b/drivers/ads1256/driver/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "ads1256" +description = "Driver for ADS1256." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +#--------------------------------------------------------------------------------------------------------------------- +#----- Dependencies ------------------------ +#--------------------------------------------------------------------------------------------------------------------- +[dependencies.ads1256-types] +path = "../types" +features = ["defmt"] +[dependencies.physical] +path = "../../.." +[dependencies.physical-node] +path = "../../../node" +[dependencies.embedded-hal] +workspace = true +[dependencies.embedded-hal-async] +workspace = true +[dependencies.defmt] +workspace = true +[dependencies.embassy-sync] +workspace = true +optional = true diff --git a/drivers/ads1256/driver/src/adc.rs b/drivers/ads1256/driver/src/adc.rs new file mode 100644 index 0000000..bd7caf3 --- /dev/null +++ b/drivers/ads1256/driver/src/adc.rs @@ -0,0 +1,562 @@ +use crate::{ + drate, mux, opcodes, status, AdControl, Ads1256, BlockingDelay, CalibrationCommand, Conversion, + DataRate, Gain, Multiplexer, Status, +}; +use embedded_hal::digital::OutputPin; +use embedded_hal::spi; +use embedded_hal::spi::SpiBus; +use embedded_hal_async::digital::Wait; +use physical_node::GPIO_ERROR_MSG; +use physical_node::spi::{end_spi, end_spi_if_err}; + +impl Ads1256 +where + DelayerT: BlockingDelay, + SST: OutputPin, + DrdyT: Wait, +{ + /// Enter read data continuous mode (wait for drdy low, issue RDATAC command) + #[inline] + pub async fn start_rdatac( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + let spi_op_result = spi.write(&[opcodes::RDATAC]); + end_spi(&mut self.slave_select, spi, spi_op_result)?; + + Ok(()) + } + + /// Exit read data continuous mode (wait for drdy low, issue SDATAC command) + #[inline] + pub async fn stop_rdatac( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + let spi_op_result = spi.write(&[opcodes::SDATAC]); + end_spi(&mut self.slave_select, spi, spi_op_result)?; + + Ok(()) + } + + pub async fn read_data( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + let mut buffer = [0u8; 3]; + let spi_op_result = spi.read(&mut buffer); + end_spi(&mut self.slave_select, spi, spi_op_result)?; + + Ok(Conversion::from_reading(buffer)) + } + + /// Issues RDATA command and receives result, does not handle any housekeeping such as setting + /// slave select or flushing the bus on successful completion. + #[inline] + pub fn raw_cmd_read_data( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + let spi_op_result = spi.write(&[opcodes::RDATA]); + end_spi_if_err(&mut self.slave_select, spi, spi_op_result)?; + self.delayer.t6_delay(); + let mut buffer = [0u8; 3]; + spi.read(&mut buffer)?; + + Ok(Conversion::from_reading(buffer)) + } + + /// Reads the digital conversion value. + /// Action sequence: + /// 1. Wait for data_ready to go low + /// 1. Issue RDATA command + /// 1. Read the conversion value + #[inline] + pub async fn cmd_read_data( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + let spi_op_result = self.raw_cmd_read_data(spi); + + end_spi(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + pub fn conversion_init( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + // Issue sync command + let spi_op_result = spi.write(&[opcodes::SYNC]); + end_spi_if_err(&mut self.slave_select, spi, spi_op_result)?; + // Issue wakeup command + let spi_op_result = spi.write(&[opcodes::WAKEUP]); + end_spi(&mut self.slave_select, spi, spi_op_result) + } + + //----- Configure Convert Functions ---------------------------------- + + /// Function for configuring and reading AD conversions using auto-calibration. If + /// only the inputs are changed and no other configuration, auto-calibration will not be + /// run. + /// + /// Action sequence: + /// 1. Switch inputs and optionally adjust different configuration parameters. + /// 1. If only the input was switched without configuration changes. + /// 1. Issue sync command followed by wakeup command + /// 1. Else, auto-calibration will take place + /// 1. Wait for data_ready low + /// 1. RDATA command (read the conversion value) + /// 1. Optionally enter standby mode + /// + /// **WARNING:** Auto-calibration must be enabled for intended functionality when changing + /// [Status], [AdControl], or [DataRate]. Furthermore if setting [Status] or [AdControl], their + /// [Buffer] and [Gain] settings must be modified respectively to trigger auto-calibration. + #[inline] + pub async fn autocal_convert( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + status: Option, + ad_control: Option, + data_rate: Option, + standby: bool, + ) -> Result::Error> { + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + match (status, ad_control, data_rate) { + // Only modifying the multiplexer, not changing any configuration + (None, None, None) => { + self._none_config(spi, input)?; + self._manual_conversion_init(spi)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying status (toggle buffer) and changing multiplexer + (Some(status), None, None) => { + self._status_config(spi, input, status)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying AD control (gain) and changing multiplexer + (None, Some(ad_control), None) => { + self._ad_config(spi, input, ad_control)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying data rate and change multiplexer + (None, None, Some(data_rate)) => { + self._drate_config(spi, input, data_rate)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying status (toggle buffer), AD control (gain), and changing multiplexer + (Some(status), Some(ad_control), None) => { + self._status_ad_config(spi, input, status, ad_control)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying status (toggle buffer), data rate, and changing multiplexer + (Some(status), None, Some(data_rate)) => { + self._status_drate_config(spi, input, status, data_rate)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying AD control (gain), data rate, and changing multiplexer + (None, Some(ad_control), Some(data_rate)) => { + self._ad_drate_config(spi, input, ad_control, data_rate)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying status (toggle buffer), AD control (gain), data rate, and changing + // multiplexer + (Some(status), Some(ad_control), Some(data_rate)) => { + self._all_config(spi, input, status, ad_control, data_rate)?; + self._read_when_rdy(spi, standby).await + }, + } + } + + /// Function for periodically configuring and reading AD conversions using stored calibration + /// values. If only the inputs are changed and no other configuration, auto-calibration will + /// not be run. For immediate continuous multiplexing see [Ads1256::loadcal_convert_next]. + /// + /// Action sequence: + /// 1. Switch inputs and optionally adjust different configuration parameters. + /// 1. Issue sync command followed by wakeup command + /// 1. Wait for data_ready low + /// 1. RDATA command (read the conversion value) + /// 1. Optionally enter standby mode + /// + /// **WARNING:** Auto-calibration must be disabled for intended functionality when changing + /// [Status], [AdControl], or [DataRate]. + #[inline] + pub async fn loadcal_convert( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + calibration: Option<&CalibrationCommand>, + status: Option, + ad_control: Option, + data_rate: Option, + standby: bool, + ) -> Result::Error> { + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + match (status, ad_control, data_rate) { + // Only modifying the multiplexer, not changing any configuration + (None, None, None) => { + self._none_config(spi, input)?; + self._manual_conversion_init(spi)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying status (toggle buffer) and changing multiplexer + (Some(status), None, None) => { + self._status_config(spi, input, status)?; + self._loadcal_init(spi, calibration)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying AD control (gain) and changing multiplexer + (None, Some(ad_control), None) => { + self._ad_config(spi, input, ad_control)?; + self._loadcal_init(spi, calibration)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying data rate and change multiplexer + (None, None, Some(data_rate)) => { + self._drate_config(spi, input, data_rate)?; + self._loadcal_init(spi, calibration)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying status (toggle buffer), AD control (gain), and changing multiplexer + (Some(status), Some(ad_control), None) => { + self._status_ad_config(spi, input, status, ad_control)?; + self._loadcal_init(spi, calibration)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying status (toggle buffer), data rate, and changing multiplexer + (Some(status), None, Some(data_rate)) => { + self._status_drate_config(spi, input, status, data_rate)?; + self._loadcal_init(spi, calibration)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying AD control (gain), data rate, and changing multiplexer + (None, Some(ad_control), Some(data_rate)) => { + self._ad_drate_config(spi, input, ad_control, data_rate)?; + self._loadcal_init(spi, calibration)?; + self._read_when_rdy(spi, standby).await + }, + // Modifying status (toggle buffer), AD control (gain), data rate, and changing + // multiplexer + (Some(status), Some(ad_control), Some(data_rate)) => { + self._all_config(spi, input, status, ad_control, data_rate)?; + self._loadcal_init(spi, calibration)?; + self._read_when_rdy(spi, standby).await + }, + } + } + + /// Function for rapidly configuring and reading AD conversions using stored calibration + /// values. If only the inputs are changed and no other configuration, auto-calibration will + /// not be run. For periodic reading see [Ads1256::loadcal_convert]. + /// + /// Action sequence: + /// 1. Wait for data_ready low + /// 1. RDATA command (read the conversion value) + /// 1. Switch inputs and optionally adjust different configuration parameters. + /// 1. Issue sync command followed by wakeup command + /// + /// **WARNING:** Auto-calibration must be disabled for intended functionality when changing + /// [Status], [AdControl], or [DataRate]. + #[inline] + pub async fn loadcal_convert_next( + &mut self, + spi: &mut SpiT, + next_input: Multiplexer, + next_calibration: Option<&CalibrationCommand>, + next_status: Option, + next_ad_control: Option, + next_data_rate: Option, + standby: bool, + ) -> Result::Error> { + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + self._loadcal_convert_next( + spi, + next_input, + next_calibration, + next_status, + next_ad_control, + next_data_rate, + standby, + ) + .await + } + + #[inline(always)] + pub(crate) async fn _loadcal_convert_next( + &mut self, + spi: &mut SpiT, + next_input: Multiplexer, + next_calibration: Option<&CalibrationCommand>, + next_status: Option, + next_ad_control: Option, + next_data_rate: Option, + standby: bool, + ) -> Result::Error> { + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + match (next_status, next_ad_control, next_data_rate) { + // Only modifying the multiplexer, not changing any configuration + (None, None, None) => { + self._none_config(spi, next_input)?; + self._manual_conversion_init(spi)?; + self._read_mux_conversion(spi, standby) + }, + // Modifying status (toggle buffer) and changing multiplexer + (Some(status), None, None) => { + self._status_config(spi, next_input, status)?; + self._loadcal_init(spi, next_calibration)?; + self._read_mux_conversion(spi, standby) + }, + // Modifying AD control (gain) and changing multiplexer + (None, Some(ad_control), None) => { + self._ad_config(spi, next_input, ad_control)?; + self._loadcal_init(spi, next_calibration)?; + self._read_mux_conversion(spi, standby) + }, + // Modifying data rate and change multiplexer + (None, None, Some(data_rate)) => { + self._drate_config(spi, next_input, data_rate)?; + self._loadcal_init(spi, next_calibration)?; + self._read_mux_conversion(spi, standby) + }, + // Modifying status (toggle buffer), AD control (gain), and changing multiplexer + (Some(status), Some(ad_control), None) => { + self._status_ad_config(spi, next_input, status, ad_control)?; + self._loadcal_init(spi, next_calibration)?; + self._read_mux_conversion(spi, standby) + }, + // Modifying status (toggle buffer), data rate, and changing multiplexer + (Some(status), None, Some(data_rate)) => { + self._status_drate_config(spi, next_input, status, data_rate)?; + self._loadcal_init(spi, next_calibration)?; + self._read_mux_conversion(spi, standby) + }, + // Modifying AD control (gain), data rate, and changing multiplexer + (None, Some(ad_control), Some(data_rate)) => { + self._ad_drate_config(spi, next_input, ad_control, data_rate)?; + self._loadcal_init(spi, next_calibration)?; + self._read_mux_conversion(spi, standby) + }, + // Modifying status (toggle buffer), AD control (gain), data rate, and changing + // multiplexer + (Some(status), Some(ad_control), Some(data_rate)) => { + self._all_config(spi, next_input, status, ad_control, data_rate)?; + self._loadcal_init(spi, next_calibration)?; + self._read_mux_conversion(spi, standby) + }, + } + } + + /// Read a pre-defined number of samples. This will use read continuous command, the ADS1256 must be + /// properly configure before calling this function. This uses [BlockingDelay::t11_1_delay] before running so as it's + /// almost always used after configuring the ADS1256 for a specific channel. + pub async fn multi_sample( + &mut self, + spi: &mut SpiT, + ) -> Result<[Conversion; NUM_SAMPLES], ::Error> { + let mut samples: [Conversion; NUM_SAMPLES] = [0.into(); NUM_SAMPLES]; + self.delayer.t11_1_delay(); + + self.start_rdatac(spi).await?; + for i in 0..NUM_SAMPLES { + samples[i] = self.read_data(spi).await?; + } + self.delayer.t6_delay(); + self.stop_rdatac(spi).await?; + + Ok(samples) + } + + #[inline] + pub(crate) fn _none_config( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + ) -> Result<(), ::Error> { + // Change inputs + let buffer = [0u8, 0u8, input.0]; + let spi_op_result = self.raw_write_registers(spi, mux::ADDRESS, buffer); + end_spi_if_err(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + pub(crate) fn _loadcal_init( + &mut self, + spi: &mut SpiT, + calibration: Option<&CalibrationCommand>, + ) -> Result<(), ::Error> { + self.delayer.t11_1_delay(); + if let Some(calibration) = calibration { + let spi_op_result = spi.write(calibration.into()); + end_spi_if_err(&mut self.slave_select, spi, spi_op_result)?; + } + self._manual_conversion_init(spi) + } + + #[inline] + pub(crate) fn _manual_conversion_init( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + self.delayer.t11_1_delay(); + // Since we did not change configuration, ADS1256 will not auto-calibrate so we + // need to sync, wakeup + // Issue sync command + let spi_op_result = spi.write(&[opcodes::SYNC]); + end_spi_if_err(&mut self.slave_select, spi, spi_op_result)?; + // Issue wakeup command + let spi_op_result = spi.write(&[opcodes::WAKEUP]); + end_spi_if_err(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + pub(crate) fn _status_config( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + status: Status, + ) -> Result<(), ::Error> { + let buffer = [0u8, 0u8, status.0, input.0]; + let spi_op_result = self.raw_write_registers(spi, status::ADDRESS, buffer); + + end_spi_if_err(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + pub(crate) fn _ad_config( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + ad_control: AdControl, + ) -> Result<(), ::Error> { + let buffer = [0u8, 0u8, input.0, ad_control.0]; + let spi_op_result = self.raw_write_registers(spi, mux::ADDRESS, buffer); + + end_spi_if_err(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + pub(crate) fn _drate_config( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + data_rate: DataRate, + ) -> Result<(), ::Error> { + // Switch inputs + let mut buffer = [0u8, 0u8, input.0]; + let spi_op_result = self.raw_write_registers(spi, mux::ADDRESS, buffer); + end_spi_if_err(&mut self.slave_select, spi, spi_op_result)?; + self.delayer.t11_1_delay(); + // Modify data rate + buffer[2] = data_rate as u8; + let spi_op_result = self.raw_write_registers(spi, drate::ADDRESS, buffer); + + end_spi_if_err(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + pub(crate) fn _status_ad_config( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + status: Status, + ad_control: AdControl, + ) -> Result<(), ::Error> { + let buffer = [0u8, 0u8, status.0, input.0, ad_control.0]; + let spi_op_result = self.raw_write_registers(spi, status::ADDRESS, buffer); + + end_spi_if_err(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + pub(crate) fn _status_drate_config( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + status: Status, + data_rate: DataRate, + ) -> Result<(), ::Error> { + // Modify status and switch inputs + let buffer = [0u8, 0u8, status.0, input.0]; + let spi_op_result = self.raw_write_registers(spi, status::ADDRESS, buffer); + end_spi_if_err(&mut self.slave_select, spi, spi_op_result)?; + self.delayer.t11_1_delay(); + // Modify data rate + let buffer = [0u8, 0u8, data_rate as u8]; + let spi_op_result = self.raw_write_registers(spi, drate::ADDRESS, buffer); + + end_spi_if_err(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + pub(crate) fn _ad_drate_config( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + ad_control: AdControl, + data_rate: DataRate, + ) -> Result<(), ::Error> { + let buffer = [0u8, 0u8, input.0, ad_control.0, data_rate as u8]; + let spi_op_result = self.raw_write_registers(spi, mux::ADDRESS, buffer); + + end_spi_if_err(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + pub(crate) fn _all_config( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + status: Status, + ad_control: AdControl, + data_rate: DataRate, + ) -> Result<(), ::Error> { + let buffer = [0u8, 0u8, status.0, input.0, ad_control.0, data_rate as u8]; + let spi_op_result = self.raw_write_registers(spi, status::ADDRESS, buffer); + + end_spi_if_err(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + async fn _read_when_rdy( + &mut self, + spi: &mut SpiT, + standby: bool, + ) -> Result::Error> { + // Wait for data ready low + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + // Read data + self._read_mux_conversion(spi, standby) + } + + #[inline] + pub(crate) fn _read_mux_conversion( + &mut self, + spi: &mut SpiT, + standby: bool, + ) -> Result::Error> { + // Read data + let spi_op_result = self.raw_cmd_read_data(spi); + let cvalue = end_spi_if_err(&mut self.slave_select, spi, spi_op_result)?; + // if standby is true enter standby mode + if standby { + let spi_op_result = spi.write(&[opcodes::STANDBY]); + end_spi_if_err(&mut self.slave_select, spi, spi_op_result)?; + } + // end SPI + self.slave_select.set_high().expect(GPIO_ERROR_MSG); + spi.flush()?; + + Ok(cvalue) + } +} diff --git a/drivers/ads1256/driver/src/delay.rs b/drivers/ads1256/driver/src/delay.rs new file mode 100644 index 0000000..7d66e13 --- /dev/null +++ b/drivers/ads1256/driver/src/delay.rs @@ -0,0 +1,44 @@ +use embedded_hal::delay::DelayNs; + +//TODO: Change to maybe async when available instead of always blocking +pub trait BlockingDelay { + /// Delay between spi write and read in a read data command + /// Total 50 master clock cycles + fn t6_delay(&mut self); + + /// Delay after RREG, WREG, RDATA + /// Total 4 master clock cycles + fn t11_1_delay(&mut self); + + /// Delay after RDATAC, RESET, SYNC + /// Total 24 master clock cycles + fn t11_2_delay(&mut self); +} + +pub struct DefaultDelay { + delayer: DelayT, +} + +impl DefaultDelay { + #[inline(always)] + pub const fn new(delayer: DelayT) -> Self { + Self { delayer } + } +} + +impl BlockingDelay for DefaultDelay { + #[inline(always)] + fn t6_delay(&mut self) { + self.delayer.delay_us(7); + } + + #[inline(always)] + fn t11_1_delay(&mut self) { + self.delayer.delay_us(1); + } + + #[inline(always)] + fn t11_2_delay(&mut self) { + self.delayer.delay_us(4); + } +} \ No newline at end of file diff --git a/drivers/ads1256/driver/src/io.rs b/drivers/ads1256/driver/src/io.rs new file mode 100644 index 0000000..f715271 --- /dev/null +++ b/drivers/ads1256/driver/src/io.rs @@ -0,0 +1,386 @@ +use crate::{ + adcon, drate, fsc0, fsc2, gpio, mux, ofc0, opcodes, status, AdControl, Ads1256, AllCalibration, + BlockingDelay, CalibrationCommand, Config, DataRate, DigitalIo, GainCalibration, Multiplexer, + OffsetCalibration, Status, +}; +use embedded_hal::digital::OutputPin; +use embedded_hal::spi; +use embedded_hal::spi::SpiBus; +use embedded_hal_async::digital::Wait; +use physical_node::GPIO_ERROR_MSG; +use physical_node::spi::{end_spi, end_spi_if_err}; + +impl Ads1256 +where + DelayerT: BlockingDelay, + SST: OutputPin, + DrdyT: Wait, +{ + //----- Base register read/write ---------------------------------- + /// [buffer] - The data to send the the ADS1256 starting at index 2, the first two bytes are + /// reserved as the command bytes. + #[inline] + pub fn raw_write_registers( + &mut self, + spi: &mut SpiT, + start_address: u8, + mut buffer: [u8; BUF_SIZE], + ) -> Result<(), ::Error> { + let num_registers = BUF_SIZE - 2; + assert!(start_address <= fsc2::ADDRESS, "Invalid starting register address."); + //TODO: Change to compile time assertion or bound in future Rust version. + assert!(num_registers <= 11, "Cannot write more than the total number of registers."); + assert!(num_registers >= 1, "Must write at least one register."); + // num_registers represents the total number of registers to write, including the one at the + // provided address. Adjust values based on it accordingly. + + // First command byte = 4 bits for the write register opcode, followed by 4 bits for the + // starting register address. + let cmd_start = opcodes::WREG | start_address; + // Second byte = number of registers to write in addition to the register at the starting + // address. + let num_additional = num_registers as u8 - 1; + buffer[0] = cmd_start; + buffer[1] = num_additional; + + spi.write(&buffer) + } + + /// [buffer] - The data to send the the ADS1256 starting at index 2, the first two bytes are + /// reserved as the command bytes. + #[inline] + pub fn write_registers( + &mut self, + spi: &mut SpiT, + start_address: u8, + buffer: [u8; BUF_SIZE], + ) -> Result<(), ::Error> { + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + let spi_op_result = self.raw_write_registers(spi, start_address, buffer); + end_spi(&mut self.slave_select, spi, spi_op_result) + } + + fn read_registers( + &mut self, + spi: &mut SpiT, + start_address: u8, + ) -> Result<[u8; NUM], ::Error> { + assert!(start_address <= fsc2::ADDRESS, "Invalid starting register address."); + //TODO: Change to compile time assertion or bound. + assert!(NUM <= 11, "Cannot read more than the total number of registers."); + assert!(NUM >= 1, "Must read at least one register."); + // NUM represents the total number of registers to read, including the one at the provided + // address. Adjust values based on it accordingly. + + // First command byte = 4 bits for the read register opcode, followed by 4 bits for the + // starting register address. + let cmd_start = opcodes::RREG | start_address; + // Second byte = number of registers to read in addition to the register at the starting + // address. + let num_additional = NUM as u8 - 1; + let mut buffer = [0u8; NUM]; + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + let spi_op_result = spi.write(&[cmd_start, num_additional]); + end_spi_if_err(&mut self.slave_select, spi, spi_op_result)?; + self.delayer.t6_delay(); + let spi_op_result = spi.read(&mut buffer); + end_spi(&mut self.slave_select, spi, spi_op_result)?; + Ok(buffer) + } + + //----- Standalone commands ---------------------------------- + #[inline] + fn standalone_command( + &mut self, + spi: &mut SpiT, + opcode: u8, + ) -> Result<(), ::Error> { + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + let spi_op_result = spi.write(&[opcode]); + end_spi(&mut self.slave_select, spi, spi_op_result) + } + + #[inline(always)] + pub fn standby( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + self.standalone_command(spi, opcodes::STANDBY) + } + + /// Self calibration is performed after reset, therefore additional commands should not be sent + /// until data ready pin goes low indicating the calibration is complete. + #[inline(always)] + pub fn reset( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + self.standalone_command(spi, opcodes::RESET) + } + + #[inline(always)] + pub fn wake( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + self.standalone_command(spi, opcodes::WAKEUP) + } + + /// Perform self offset and gain calibration. + #[inline] + pub async fn self_calibrate( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + let result = self.standalone_command(spi, opcodes::SELFCAL); + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + result + } + + /// Perform self offset calibration. + #[inline] + pub async fn self_offset_calibrate( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + let result = self.standalone_command(spi, opcodes::SELFOCAL); + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + result + } + + /// Perform self gain calibration. + #[inline] + pub async fn self_gain_calibrate( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + let result = self.standalone_command(spi, opcodes::SELFGCAL); + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + result + } + + /// Perform system offset calibration. + #[inline] + pub async fn system_offset_calibrate( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + let result = self.standalone_command(spi, opcodes::SYSOCAL); + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + result + } + + /// Perform system gain calibration. + #[inline] + pub async fn system_gain_calibrate( + &mut self, + spi: &mut SpiT, + ) -> Result<(), ::Error> { + let result = self.standalone_command(spi, opcodes::SYSGCAL); + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + result + } + + //----- Public register read/write ---------------------------------- + #[inline] + pub fn read_status( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + Ok(Status(self.read_registers::<_, 1>(spi, status::ADDRESS)?[0])) + } + + #[inline] + pub fn write_status( + &mut self, + spi: &mut SpiT, + setting: Status, + ) -> Result<(), ::Error> { + // Create full command buffer, initialize command bytes to 0 and set data value to status + // byte. + let buffer: [u8; 3] = [0, 0, setting.0]; + self.write_registers(spi, status::ADDRESS, buffer) + } + + #[inline] + pub fn read_multiplexer( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + Ok(Multiplexer(self.read_registers::<_, 1>(spi, mux::ADDRESS)?[0])) + } + + #[inline] + pub fn write_multiplexer( + &mut self, + spi: &mut SpiT, + setting: Multiplexer, + ) -> Result<(), ::Error> { + // Create full command buffer, initialize command bytes to 0 and set data value to status + // byte. + let buffer: [u8; 3] = [0, 0, setting.0]; + self.write_registers(spi, mux::ADDRESS, buffer) + } + + #[inline] + pub fn read_ad_control( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + Ok(AdControl(self.read_registers::<_, 1>(spi, adcon::ADDRESS)?[0])) + } + + #[inline] + pub fn write_ad_control( + &mut self, + spi: &mut SpiT, + setting: AdControl, + ) -> Result<(), ::Error> { + // Create full command buffer, initialize command bytes to 0 and set data value to status + // byte. + let buffer: [u8; 3] = [0, 0, setting.0]; + self.write_registers(spi, adcon::ADDRESS, buffer) + } + + /// Combined function to write the ADC and multiplexer registers at the same time since this is such a common + /// occurrence. + #[inline] + pub fn write_mux_adc( + &mut self, + spi: &mut SpiT, + input: Multiplexer, + ad_control: AdControl, + ) -> Result<(), ::Error> { + let buffer = [0u8, 0u8, input.0, ad_control.0]; + self.write_registers(spi, mux::ADDRESS, buffer) + } + + #[inline] + pub fn read_data_rate( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + Ok(DataRate::from_byte(self.read_registers::<_, 1>(spi, drate::ADDRESS)?[0])) + } + + #[inline] + pub fn write_data_rate( + &mut self, + spi: &mut SpiT, + setting: DataRate, + ) -> Result<(), ::Error> { + // Create full command buffer, initialize command bytes to 0 and set data value to status + // byte. + let buffer: [u8; 3] = [0, 0, setting as u8]; + self.write_registers(spi, drate::ADDRESS, buffer) + } + + #[inline] + pub fn read_gpio( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + Ok(DigitalIo(self.read_registers::<_, 1>(spi, gpio::ADDRESS)?[0])) + } + + #[inline] + pub fn write_gpio( + &mut self, + spi: &mut SpiT, + setting: DigitalIo, + ) -> Result<(), ::Error> { + // Create full command buffer, initialize command bytes to 0 and set data value to status + // byte. + let buffer: [u8; 3] = [0, 0, setting.0]; + self.write_registers(spi, gpio::ADDRESS, buffer) + } + + #[inline] + pub fn read_offset_calibration( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + Ok(OffsetCalibration(self.read_registers(spi, ofc0::ADDRESS)?)) + } + + #[inline] + pub fn write_offset_calibration( + &mut self, + spi: &mut SpiT, + setting: OffsetCalibration, + ) -> Result<(), ::Error> { + // Create full command buffer, initialize command bytes to 0 and set data values to offset + // calibration bytes + let buffer = [0, 0, setting.0[0], setting.0[1], setting.0[2]]; + self.write_registers(spi, ofc0::ADDRESS, buffer) + } + + #[inline] + pub fn read_gain_calibration( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + Ok(GainCalibration(self.read_registers(spi, fsc0::ADDRESS)?)) + } + + #[inline] + pub fn write_gain_calibration( + &mut self, + spi: &mut SpiT, + setting: GainCalibration, + ) -> Result<(), ::Error> { + // Create full command buffer, initialize command bytes to 0 and set data values to gain + // calibration bytes + let buffer = [0, 0, setting.0[0], setting.0[1], setting.0[2]]; + self.write_registers(spi, fsc0::ADDRESS, buffer) + } + + /// Reads all calibration registers. Bytes 0 to 2 are offset calibration, bytes 3 to 5 are gain + /// calibration. + #[inline] + pub fn read_all_calibration( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + Ok(self.read_registers(spi, ofc0::ADDRESS)?.into()) + } + + #[inline] + pub fn exec_cal_command( + &mut self, + spi: &mut SpiT, + command: &CalibrationCommand, + ) -> Result<(), ::Error> { + let spi_op_result = spi.write(command.into()); + end_spi(&mut self.slave_select, spi, spi_op_result) + } + + #[inline] + pub fn read_config( + &mut self, + spi: &mut SpiT, + ) -> Result::Error> { + let bytes = self.read_registers::<_, 5>(spi, status::ADDRESS)?; + Ok(Config::from_bytes(bytes)) + } + + #[inline] + pub fn write_config( + &mut self, + spi: &mut SpiT, + setting: Config, + ) -> Result<(), ::Error> { + // Create full command buffer, initialize command bytes to 0 and set data value to status + // byte. + let buffer: [u8; 7] = [ + 0, + 0, + setting.status.0, + setting.multiplexer.0, + setting.ad_control.0, + setting.data_rate as u8, + setting.digital_io.0, + ]; + self.write_registers(spi, status::ADDRESS, buffer) + } +} diff --git a/drivers/ads1256/driver/src/lib.rs b/drivers/ads1256/driver/src/lib.rs new file mode 100644 index 0000000..d242ab8 --- /dev/null +++ b/drivers/ads1256/driver/src/lib.rs @@ -0,0 +1,52 @@ +#![no_std] + +mod adc; +mod delay; +mod io; +#[cfg(feature = "embassy-sync")] +mod mutex; + +pub use crate::adc::*; +pub use crate::delay::*; +pub use crate::io::*; +#[cfg(feature = "embassy-sync")] +pub use crate::mutex::*; +pub use ads1256_types::adcon::{ClockOut, Gain, Sdcs}; +pub use ads1256_types::drate::DataRate; +pub use ads1256_types::gpio::{DState, DioDirection}; +pub use ads1256_types::mux::MuxInput; +pub use ads1256_types::status::{AutoCal, BitOrder, Buffer}; +pub use ads1256_types::*; + +pub use embedded_hal::digital::OutputPin; +pub use embedded_hal::spi::SpiBus; +pub use embedded_hal_async::digital::Wait; + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Ads1256 ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +/// **WARNING:** All [Ads1256] methods only ever wait in the middle of a multi-command method, in +/// some cases you may need to use [BlockingDelay::t11_1_delay] or [BlockingDelay::t11_2_delay] +/// between methods that issue commands if they are done one immediately following the other. +pub struct Ads1256 { + pub delayer: DelayerT, + slave_select: SST, + pub data_ready: DrdyT, +} + +impl Ads1256 +where + DelayerT: BlockingDelay, + SST: OutputPin, + DrdyT: Wait, +{ + //----- New ---------------------------------- + #[inline(always)] + pub fn new(delayer: DelayerT, slave_select: SST, data_ready: DrdyT) -> Self { + Self { + delayer, + slave_select, + data_ready, + } + } +} diff --git a/drivers/ads1256/driver/src/mutex.rs b/drivers/ads1256/driver/src/mutex.rs new file mode 100644 index 0000000..c7e9b10 --- /dev/null +++ b/drivers/ads1256/driver/src/mutex.rs @@ -0,0 +1,271 @@ +use crate::{AdControl, Ads1256, BlockingDelay, CalibrationCommand, Conversion, + DataRate, Multiplexer, Status, +}; +use core::ops::DerefMut; +use embassy_sync::blocking_mutex::raw::RawMutex; + +use physical_node::GPIO_ERROR_MSG; +use physical_node::spi::end_spi; +use embassy_sync::mutex::Mutex; +use embedded_hal::digital::OutputPin; +use embedded_hal::spi; +use embedded_hal::spi::SpiBus; +use embedded_hal_async::digital::Wait; + +impl Ads1256 +where + DelayerT: BlockingDelay, + SST: OutputPin, + DrdyT: Wait, +{ + /// Functionally the same as [Ads1256::cmd_read_data] but exercises fine-grained control + /// over the [Mutex] of a [SpiBus] in cases where one is used. This function will unlock the + /// [Mutex] while it is waiting for data from the ADS1256. + /// + /// Action sequence: + /// 1. Wait for data_ready to go low + /// 1. Lock mutex, mutably borrow SPI + /// 1. Read the conversion value + #[inline] + pub async fn cmd_read_data_m( + &mut self, + spi: &Mutex, + ) -> Result::Error> { + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + let mut spi_guard = spi.lock().await; + let spi = spi_guard.deref_mut(); + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + let spi_op_result = self.raw_cmd_read_data(spi); + + end_spi(&mut self.slave_select, spi, spi_op_result) + } + + /// Functionally the same as [Ads1256::autocal_convert] but exercises fine-grained control + /// over the [Mutex] of a [SpiBus] in cases where one is used. This function will unlock the + /// [Mutex] while it is waiting for data from the ADS1256. + /// + /// Action sequence: + /// 1. Switch inputs and optionally adjust different configuration parameters. + /// 1. If only the input was switched without configuration changes. + /// 1. Issue sync command followed by wakeup command + /// 1. Else, auto-calibration will take place + /// 1. Wait for data_ready low + /// 1. RDATA command (read the conversion value) + /// 1. Optionally enter standby mode + /// + /// **WARNING:** Auto-calibration must be enabled for intended functionality when changing + /// [Status], [AdControl], or [DataRate]. Furthermore if setting [Status] or [AdControl], their + /// [Buffer] and [Gain] settings must be modified respectively to trigger auto-calibration. + #[inline] + pub async fn autocal_convert_m( + &mut self, + spi_mutex: &Mutex, + input: Multiplexer, + status: Option, + ad_control: Option, + data_rate: Option, + standby: bool, + ) -> Result::Error> { + // Acquire SPI lock + let mut spi_guard = spi_mutex.lock().await; + let spi = spi_guard.deref_mut(); + // Slave select low + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + match (status, ad_control, data_rate) { + // Only modifying the multiplexer, not changing any configuration + (None, None, None) => { + self._none_config(spi, input)?; + self._manual_conversion_init(spi)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying status (toggle buffer) and changing multiplexer + (Some(status), None, None) => { + self._status_config(spi, input, status)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying AD control (gain) and changing multiplexer + (None, Some(ad_control), None) => { + self._ad_config(spi, input, ad_control)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying data rate and change multiplexer + (None, None, Some(data_rate)) => { + self._drate_config(spi, input, data_rate)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying status (toggle buffer), AD control (gain), and changing multiplexer + (Some(status), Some(ad_control), None) => { + self._status_ad_config(spi, input, status, ad_control)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying status (toggle buffer), data rate, and changing multiplexer + (Some(status), None, Some(data_rate)) => { + self._status_drate_config(spi, input, status, data_rate)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying AD control (gain), data rate, and changing multiplexer + (None, Some(ad_control), Some(data_rate)) => { + self._ad_drate_config(spi, input, ad_control, data_rate)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying status (toggle buffer), AD control (gain), data rate, and changing + // multiplexer + (Some(status), Some(ad_control), Some(data_rate)) => { + self._all_config(spi, input, status, ad_control, data_rate)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + } + } + + /// Functionally the same as [Ads1256::loadcal_convert] but exercises fine-grained control + /// over the [Mutex] of a [SpiBus] in cases where one is used. This function will unlock the + /// [Mutex] while it is waiting for data from the ADS1256. + /// + /// Action sequence: + /// 1. Switch inputs and optionally adjust different configuration parameters. + /// 1. Issue sync command followed by wakeup command + /// 1. Wait for data_ready low + /// 1. RDATA command (read the conversion value) + /// 1. Optionally enter standby mode + /// + /// **WARNING:** Auto-calibration must be disabled for intended functionality when changing + /// [Status], [AdControl], or [DataRate]. + #[inline] + pub async fn loadcal_convert_m( + &mut self, + spi_mutex: &Mutex, + input: Multiplexer, + calibration: Option<&CalibrationCommand>, + status: Option, + ad_control: Option, + data_rate: Option, + standby: bool, + ) -> Result::Error> { + // Acquire SPI lock + let mut spi_guard = spi_mutex.lock().await; + let spi = spi_guard.deref_mut(); + // Slave select low + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + match (status, ad_control, data_rate) { + // Only modifying the multiplexer, not changing any configuration + (None, None, None) => { + self._none_config(spi, input)?; + self._manual_conversion_init(spi)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying status (toggle buffer) and changing multiplexer + (Some(status), None, None) => { + self._status_config(spi, input, status)?; + self._loadcal_init(spi, calibration)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying AD control (gain) and changing multiplexer + (None, Some(ad_control), None) => { + self._ad_config(spi, input, ad_control)?; + self._loadcal_init(spi, calibration)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying data rate and change multiplexer + (None, None, Some(data_rate)) => { + self._drate_config(spi, input, data_rate)?; + self._loadcal_init(spi, calibration)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying status (toggle buffer), AD control (gain), and changing multiplexer + (Some(status), Some(ad_control), None) => { + self._status_ad_config(spi, input, status, ad_control)?; + self._loadcal_init(spi, calibration)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying status (toggle buffer), data rate, and changing multiplexer + (Some(status), None, Some(data_rate)) => { + self._status_drate_config(spi, input, status, data_rate)?; + self._loadcal_init(spi, calibration)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying AD control (gain), data rate, and changing multiplexer + (None, Some(ad_control), Some(data_rate)) => { + self._ad_drate_config(spi, input, ad_control, data_rate)?; + self._loadcal_init(spi, calibration)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + // Modifying status (toggle buffer), AD control (gain), data rate, and changing + // multiplexer + (Some(status), Some(ad_control), Some(data_rate)) => { + self._all_config(spi, input, status, ad_control, data_rate)?; + self._loadcal_init(spi, calibration)?; + spi.flush()?; + drop(spi_guard); + self._read_when_rdy_m(spi_mutex, standby).await + }, + } + } + + #[inline] + pub async fn loadcal_convert_next_m( + &mut self, + spi_mutex: &Mutex, + next_input: Multiplexer, + next_calibration: Option<&CalibrationCommand>, + next_status: Option, + next_ad_control: Option, + next_data_rate: Option, + standby: bool, + ) -> Result::Error> { + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + self._loadcal_convert_next( + spi_mutex.lock().await.deref_mut(), + next_input, + next_calibration, + next_status, + next_ad_control, + next_data_rate, + standby, + ) + .await + } + + #[inline] + async fn _read_when_rdy_m( + &mut self, + spi_mutex: &Mutex, + standby: bool, + ) -> Result::Error> { + self.slave_select.set_high().expect(GPIO_ERROR_MSG); + // Wait for data ready low + self.data_ready.wait_for_low().await.expect(GPIO_ERROR_MSG); + self.slave_select.set_low().expect(GPIO_ERROR_MSG); + // Reacquire lock on SPI mutex and read mux conversion + self._read_mux_conversion(spi_mutex.lock().await.deref_mut(), standby) + } +} diff --git a/drivers/ads1256/types/Cargo.toml b/drivers/ads1256/types/Cargo.toml new file mode 100644 index 0000000..faeeb3c --- /dev/null +++ b/drivers/ads1256/types/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ads1256-types" +description = "ADS1256 data types." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies.physical] +path = "../../.." +[dependencies.defmt] +workspace = true +optional = true diff --git a/drivers/ads1256/types/src/constants.rs b/drivers/ads1256/types/src/constants.rs new file mode 100644 index 0000000..f35d7c6 --- /dev/null +++ b/drivers/ads1256/types/src/constants.rs @@ -0,0 +1,304 @@ +pub const REFERENCE_VOLTS: f32 = 2.5; +pub const MAX_CONVERSION_VALUE: i32 = 8_388_607; + +pub mod defaults { + //----- Public ---------------------------------- + pub const SPI_CLK_HZ: u32 = 1_920_000; +} + +pub mod opcodes { + /// Completes SYNC and Exits Standby Mode. + pub const WAKEUP: u8 = 0b00000000; + /// Read Data. + pub const RDATA: u8 = 0b00000001; + /// Read Data Continuously. + pub const RDATAC: u8 = 0b00000011; + /// Stop Read Data Continuously + pub const SDATAC: u8 = 0b00001111; + /// Read from register at rrrr. The command is only the first 4 bits, the last 4 bits need to be + /// changed to the register address. + pub const RREG: u8 = 0b0001_0000; + /// Write to register at rrrr. The command is only the first 4 bits, the last 4 bits need to be + /// changed to the register address. + pub const WREG: u8 = 0b0101_0000; + /// Offset and Gain Self-Calibration. + pub const SELFCAL: u8 = 0b11110000; + /// Offset Self-Calibration. + pub const SELFOCAL: u8 = 0b11110001; + /// Gain Self-Calibration. + pub const SELFGCAL: u8 = 0b11110010; + /// System Offset Calibration. + pub const SYSOCAL: u8 = 0b11110011; + /// System Gain Calibration. + pub const SYSGCAL: u8 = 0b11110100; + /// Synchronize the A/D Conversion. + pub const SYNC: u8 = 0b11111100; + /// Begin Standby Mode. + pub const STANDBY: u8 = 0b11111101; + /// Reset to Power-Up Values. + pub const RESET: u8 = 0b11111110; +} + +/// Status register. +pub mod status { + /// Address of the STATUS register. + pub const ADDRESS: u8 = 0b0000_0000; + + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum Buffer { + Enabled = 0b000000_1_0, + Disabled = 0, + } + + impl Buffer { + pub const MASK: u8 = 0b000000_1_0; + } + + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum AutoCal { + Enabled = 0b00000_1_00, + Disabled = 0, + } + + impl AutoCal { + pub const MASK: u8 = 0b00000_1_00; + } + + /// Input data is always shifted in most significant byte and bit first. Output data is always + /// shifted out most significant byte first. The [BitOrder] only controls the bit order of the + /// output data within the byte. + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum BitOrder { + /// Most significant bit first. + MostSigFirst = 0, + /// Least significant bit first. + LeastSigFirst = 0b0000_1_000, + } + + impl BitOrder { + pub const MASK: u8 = 0b0000_1_000; + } +} + +/// Input multiplexer control register. +pub mod mux { + /// address of the MUX register. + pub const ADDRESS: u8 = 0b0000_0001; + + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum MuxInput { + AIn0 = 0b0000_0000, + AIn1 = 0b0000_0001, + AIn2 = 0b0000_0010, + AIn3 = 0b0000_0011, + AIn4 = 0b0000_0100, + AIn5 = 0b0000_0101, + AIn6 = 0b0000_0110, + AIn7 = 0b0000_0111, + Common = 0b0000_1000, + } + + impl MuxInput { + pub const NEGATIVE_MASK: u8 = 0b0000_1111; + pub const POSITIVE_MASK: u8 = 0b1111_0000; + } +} + +/// A/D control register. +pub mod adcon { + /// Address of the ADCON register. + pub const ADDRESS: u8 = 0b0000_0010; + + //TODO: Fitting the value and encoding in 16 bits can probably be done more cleanly if enum + // variants become types in a later version of Rust. + /// **Warning:** casting to an integer will likely not yield expected result. Use `value()` or + /// `encoding()` methods instead. + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum Gain { + X1 = 0b00000_000, + X2 = 0b00000_001, + X4 = 0b00000_010, + X8 = 0b00000_011, + X16 = 0b00000_100, + X32 = 0b00000_101, + X64 = 0b00000_110, + } + + impl Gain { + /// Value to use binary & operator on the full ADCON byte to isolate the gain. + pub const MASK: u8 = 0b00000_111; + pub const ALT_X64: u8 = 0b00000_111; + + /// The integer value of this [Gain]. + /// + /// ``` + /// use ads1256_types::Gain; + /// + /// assert_eq!(Gain::X1.value(), 1); + /// assert_eq!(Gain::X2.value(), 2); + /// assert_eq!(Gain::X4.value(), 4); + /// assert_eq!(Gain::X8.value(), 8); + /// assert_eq!(Gain::X16.value(), 16); + /// assert_eq!(Gain::X32.value(), 32); + /// assert_eq!(Gain::X64.value(), 64); + /// ``` + #[inline(always)] + pub const fn value(self) -> u8 { + // Multiply 1 by 2 the number of times specified in the encoding (e.g. for gain of 32, + // (1 * 2) 5 times (5 is the encoding for 32) is 32). + 1 << (self as u8) + } + } + + /// Sensor detect current sources. + /// For testing that sensor is still connected and able to pass current. + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum Sdcs { + Off = 0b000_00_000, + /// 0.5µA + C05 = 0b000_01_000, + /// 2µA + C2 = 0b000_10_000, + /// 10µA + C10 = 0b000_11_000, + } + + impl Sdcs { + /// Value to use binary & operator on the full ADCON byte to isolate the SDCS. + pub const MASK: u8 = 0b000_11_000; + } + + /// Ads1256 master clock cycle frequency outputted through GPIO. + #[allow(non_camel_case_types)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum ClockOut { + Off = 0b0_00_00000, + /// Equal to clock in frequency. + ClkIn = 0b0_01_00000, + /// Equal to clock in frequency / 2. + ClkIn_2 = 0b0_10_00000, + /// Equal to clock in frequency / 4. + ClkIn_4 = 0b0_11_00000, + } + + impl ClockOut { + /// Value to use binary & operator on the full ADCON byte to isolate the clock out + /// frequency. + pub const MASK: u8 = 0b0_11_00000; + } +} + +/// A/D data rate register. +pub mod drate { + /// Address of the DRATE register. + pub const ADDRESS: u8 = 0b0000_0011; + + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum DataRate { + Sps2_5 = 0b00000011, + Sps5 = 0b00010011, + Sps10 = 0b00100011, + Sps15 = 0b00110011, + Sps25 = 0b01000011, + Sps30 = 0b01010011, + Sps50 = 0b01100011, + Sps60 = 0b01110010, + Sps100 = 0b10000010, + Sps500 = 0b10010010, + Sps1000 = 0b10100001, + Sps2000 = 0b10110000, + Sps3750 = 0b11000000, + Sps7500 = 0b11010000, + Sps15000 = 0b11100000, + Sps30000 = 0b11110000, + } +} + +pub mod gpio { + /// Address of the IO register. + pub const ADDRESS: u8 = 0b0000_0100; + + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum DioDirection { + Output = 0, + Input = 1, + } + + impl DioDirection { + pub const ALL_MASK: u8 = 0b1111_0000; + pub const IO0_MASK: u8 = 0b10000; + pub const IO1_MASK: u8 = 0b100000; + pub const IO2_MASK: u8 = 0b1000000; + pub const IO3_MASK: u8 = 0b10000000; + } + + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum DState { + Low = 0, + High = 1, + } + + impl DState { + pub const ALL_MASK: u8 = 0b0000_1111; + pub const IO0_MASK: u8 = 0b1; + pub const IO1_MASK: u8 = 0b10; + pub const IO2_MASK: u8 = 0b100; + pub const IO3_MASK: u8 = 0b1000; + } +} + +/// Offset calibration byte 0. +pub mod ofc0 { + /// Address of the OFC0 register. + pub const ADDRESS: u8 = 0b0000_0101; +} + +/// Offset calibration byte 1. +pub mod ofc1 { + /// Address of the OFC1 register. + pub const ADDRESS: u8 = 0b0000_0110; +} + +/// Offset calibration byte 2. +pub mod ofc2 { + /// Address of the OFC2 register. + pub const ADDRESS: u8 = 0b0000_0111; +} + +/// Full scale calibration byte 0 +pub mod fsc0 { + /// Address of the FSC0 register. + pub const ADDRESS: u8 = 0b0000_1000; +} + +/// Full scale calibration byte 0 +pub mod fsc1 { + /// Address of the FSC1 register. + pub const ADDRESS: u8 = 0b0000_1001; +} + +/// Full scale calibration byte 0 +pub mod fsc2 { + /// Address of the FSC2 register. + pub const ADDRESS: u8 = 0b0000_1010; +} diff --git a/drivers/ads1256/types/src/conversion.rs b/drivers/ads1256/types/src/conversion.rs new file mode 100644 index 0000000..5359d59 --- /dev/null +++ b/drivers/ads1256/types/src/conversion.rs @@ -0,0 +1,34 @@ +use crate::{Gain, MAX_CONVERSION_VALUE, REFERENCE_VOLTS}; +use physical::quantity::Volts; + +/// Raw digital value resulting from the conversion of an analog signal by the ADS1256. +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +#[repr(transparent)] +pub struct Conversion(pub i32); + +impl Conversion { + /// Process the conversion byte reading and return the raw conversion value. + #[inline] + pub fn from_reading(reading: [u8; 3]) -> Self { + let lsb = reading[2] as i32; + let mb = reading[1] as i32; + let msb = (reading[0] as i8) as i32; // double cast for sign extension + + Self((msb << 16) | (mb << 8) | lsb) + } + + pub fn to_voltage(self, gain: Gain) -> Volts { + let volts = ((2.0 * REFERENCE_VOLTS) / (MAX_CONVERSION_VALUE as f32)) + * (self.0 as f32 / gain.value() as f32); + + Volts(volts) + } +} + +impl From for Conversion { + #[inline(always)] + fn from(value: i32) -> Self { + Conversion(value) + } +} diff --git a/drivers/ads1256/types/src/lib.rs b/drivers/ads1256/types/src/lib.rs new file mode 100644 index 0000000..c54daf6 --- /dev/null +++ b/drivers/ads1256/types/src/lib.rs @@ -0,0 +1,15 @@ +#![no_std] + +mod constants; +mod registers; +pub mod standard; +mod conversion; + +pub use crate::constants::adcon::{ClockOut, Gain, Sdcs}; +pub use crate::constants::drate::DataRate; +pub use crate::constants::gpio::{DState, DioDirection}; +pub use crate::constants::mux::MuxInput; +pub use crate::constants::status::{AutoCal, BitOrder, Buffer}; +pub use crate::constants::*; +pub use crate::registers::*; +pub use crate::conversion::*; \ No newline at end of file diff --git a/drivers/ads1256/types/src/registers.rs b/drivers/ads1256/types/src/registers.rs new file mode 100644 index 0000000..4968f61 --- /dev/null +++ b/drivers/ads1256/types/src/registers.rs @@ -0,0 +1,645 @@ +use crate::{ + ofc0, opcodes, AutoCal, BitOrder, Buffer, ClockOut, DState, DataRate, DioDirection, Gain, + MuxInput, Sdcs, +}; +use core::mem; + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Calibration ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[repr(transparent)] +pub struct OffsetCalibration(pub [u8; 3]); + +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[repr(transparent)] +pub struct GainCalibration(pub [u8; 3]); + +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[repr(C)] +pub struct AllCalibration { + pub offset: OffsetCalibration, + pub gain: GainCalibration, +} + +impl From<[u8; 6]> for AllCalibration { + /// This array must be formatted as offset cal bytes [0..2] followed by gain cal bytes [0..2] + /// for resulting [AllCalibration] to be meaningful. + #[inline(always)] + fn from(array: [u8; 6]) -> Self { + unsafe { mem::transmute::<[u8; 6], Self>(array) } + } +} + +impl Into<[u8; 6]> for AllCalibration { + #[inline(always)] + fn into(self) -> [u8; 6] { + unsafe { mem::transmute::(self) } + } +} + +/// The complete formatted command to write all calibration values to ADS1256 registers. +/// Prepending the write command bytes to the calibration value is a fairly costly operation and in +/// practice the only real use of calibration values is to store them and write them to the ADS1256. +/// When multiplexing inputs we store calibration infrequently and write it frequently so it's +/// better to take the overhead of prepending the command bytes when we store, not when we write. +#[derive(Copy, Clone, Eq, PartialEq)] +#[repr(C)] +pub struct CalibrationCommand { + command: [u8; 2], + calibration: AllCalibration, +} + +impl From for CalibrationCommand { + fn from(calibration: AllCalibration) -> Self { + Self { + command: [opcodes::WREG | ofc0::ADDRESS, 5], + calibration, + } + } +} + +impl Into<[u8; 8]> for CalibrationCommand { + #[inline(always)] + fn into(self) -> [u8; 8] { + unsafe { mem::transmute::(self) } + } +} + +impl<'a> Into<&'a [u8]> for &'a CalibrationCommand { + #[inline(always)] + fn into(self) -> &'a [u8] { + unsafe { mem::transmute::(self) } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Config ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[derive(Copy, Clone, Eq, PartialEq)] +#[repr(C)] +pub struct Config { + pub status: Status, + pub multiplexer: Multiplexer, + pub ad_control: AdControl, + pub data_rate: DataRate, + pub digital_io: DigitalIo, +} + +impl Config { + // We want this internal and are only using it in [Ads1256::read_config]. + #[inline] + pub const fn from_bytes(bytes: [u8; 5]) -> Self { + Self { + status: Status(bytes[0]), + multiplexer: Multiplexer(bytes[1]), + ad_control: AdControl(bytes[2]), + data_rate: DataRate::from_byte(bytes[3]), + digital_io: DigitalIo(bytes[4]), + } + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Config { + #[rustfmt::skip] + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + "Config {{\n\ + \t{},\n\ + \t{},\n\ + \t{},\n\ + \tdate_rate: {},\n\ + \t{}\n\ + }}", + self.status, + self.multiplexer, + self.ad_control, + self.data_rate, + self.digital_io + ) + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Status ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[derive(Copy, Clone, Eq, PartialEq)] +#[repr(transparent)] +pub struct Status(pub u8); + +impl Status { + /// Creates a new [Status] with all writable properties explicitly set. + #[inline(always)] + pub const fn setting(buffer: Buffer, auto_calibration: AutoCal, bit_order: BitOrder) -> Status { + Status(buffer as u8 | auto_calibration as u8 | bit_order as u8) + } + + /// returns a copy of the [Status] with the buffer setting replaced by the [setting] parameter. + #[inline(always)] + pub const fn with_buffer(self, setting: Buffer) -> Status { + let zeroed_setting = self.0 & !Buffer::MASK; + Status(zeroed_setting | setting as u8) + } + + /// returns a copy of the [Status] with the auto-calibration setting replaced by the [setting] + /// parameter. + #[inline(always)] + pub const fn with_auto_cal(self, setting: AutoCal) -> Status { + let zeroed_setting = self.0 & !AutoCal::MASK; + Status(zeroed_setting | setting as u8) + } + + #[inline(always)] + pub const fn data_ready(self) -> bool { + const MASK: u8 = 0b1; + unsafe { mem::transmute::(self.0 & MASK) } + } + + #[inline(always)] + pub const fn buffer(self) -> Buffer { + unsafe { mem::transmute::(self.0 & Buffer::MASK) } + } + + #[inline(always)] + pub const fn auto_calibration(self) -> AutoCal { + unsafe { mem::transmute::(self.0 & AutoCal::MASK) } + } + + /// Data output bit order. + /// + /// Input data is always shifted in most significant byte and bit first. Output data is always + /// shifted out most significant byte first. The ORDER bit only controls the bit order of the + /// output data within the byte. + #[inline(always)] + pub const fn data_output_bit_order(self) -> BitOrder { + unsafe { mem::transmute::(self.0 & BitOrder::MASK) } + } + + #[inline(always)] + pub const fn id(self) -> u8 { + self.0 >> 4 + } +} + +impl Into for Status { + #[inline(always)] + fn into(self) -> u8 { + self.0 + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Status { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + "Status(data_ready: {}, buffer: {}, auto_calibration: {}, data_output_bit_order: {}, \ + id: {})", + self.data_ready(), + self.buffer(), + self.auto_calibration(), + self.data_output_bit_order(), + self.id() + ) + } +} + +impl Buffer { + #[inline(always)] + pub const fn is_enabled(self) -> bool { + match self { + Buffer::Enabled => true, + Buffer::Disabled => false, + } + } +} + +impl AutoCal { + #[inline(always)] + pub const fn is_enabled(self) -> bool { + match self { + AutoCal::Enabled => true, + AutoCal::Disabled => false, + } + } +} +// --------------------------------------------------------------------------------------------------------------------- +// ----- Input Multiplexer ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[derive(Copy, Clone, Eq, PartialEq)] +#[repr(transparent)] +pub struct Multiplexer(pub u8); + +impl Multiplexer { + #[inline(always)] + pub const fn setting(positive_input: MuxInput, negative_input: MuxInput) -> Multiplexer { + Multiplexer(((positive_input as u8) << 4) | negative_input as u8) + } + + #[inline(always)] + pub const fn with_positive(self, input: MuxInput) -> Multiplexer { + let zeroed_positive = self.0 & !MuxInput::POSITIVE_MASK; + Multiplexer(zeroed_positive | ((input as u8) << 4)) + } + + #[inline(always)] + pub const fn with_negative(self, input: MuxInput) -> Multiplexer { + let zeroed_negative = self.0 & !MuxInput::NEGATIVE_MASK; + Multiplexer(zeroed_negative | input as u8) + } + + #[inline] + pub const fn positive_input(self) -> MuxInput { + const A_IN0: u8 = (MuxInput::AIn0 as u8) << 4; + const A_IN1: u8 = (MuxInput::AIn1 as u8) << 4; + const A_IN2: u8 = (MuxInput::AIn2 as u8) << 4; + const A_IN3: u8 = (MuxInput::AIn3 as u8) << 4; + const A_IN4: u8 = (MuxInput::AIn4 as u8) << 4; + const A_IN5: u8 = (MuxInput::AIn5 as u8) << 4; + const A_IN6: u8 = (MuxInput::AIn6 as u8) << 4; + const A_IN7: u8 = (MuxInput::AIn7 as u8) << 4; + + match self.0 & MuxInput::POSITIVE_MASK { + A_IN0 => MuxInput::AIn0, + A_IN1 => MuxInput::AIn1, + A_IN2 => MuxInput::AIn2, + A_IN3 => MuxInput::AIn3, + A_IN4 => MuxInput::AIn4, + A_IN5 => MuxInput::AIn5, + A_IN6 => MuxInput::AIn6, + A_IN7 => MuxInput::AIn7, + _ => MuxInput::Common, + } + } + + #[inline] + pub const fn negative_input(self) -> MuxInput { + const A_IN0: u8 = MuxInput::AIn0 as u8; + const A_IN1: u8 = MuxInput::AIn1 as u8; + const A_IN2: u8 = MuxInput::AIn2 as u8; + const A_IN3: u8 = MuxInput::AIn3 as u8; + const A_IN4: u8 = MuxInput::AIn4 as u8; + const A_IN5: u8 = MuxInput::AIn5 as u8; + const A_IN6: u8 = MuxInput::AIn6 as u8; + const A_IN7: u8 = MuxInput::AIn7 as u8; + + match self.0 & MuxInput::NEGATIVE_MASK { + A_IN0 => MuxInput::AIn0, + A_IN1 => MuxInput::AIn1, + A_IN2 => MuxInput::AIn2, + A_IN3 => MuxInput::AIn3, + A_IN4 => MuxInput::AIn4, + A_IN5 => MuxInput::AIn5, + A_IN6 => MuxInput::AIn6, + A_IN7 => MuxInput::AIn7, + _ => MuxInput::Common, + } + } +} + +impl Into for Multiplexer { + #[inline(always)] + fn into(self) -> u8 { + self.0 + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Multiplexer { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + "Multiplexer(positive_input: {}, negative_input: {})", + self.positive_input(), + self.negative_input() + ) + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- A/D Control ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[derive(Copy, Clone, Eq, PartialEq)] +#[repr(transparent)] +pub struct AdControl(pub u8); + +impl AdControl { + #[inline(always)] + pub const fn setting( + gain: Gain, + sensor_detect_current: Sdcs, + clock_out: ClockOut, + ) -> AdControl { + AdControl(gain as u8 | sensor_detect_current as u8 | clock_out as u8) + } + + #[inline(always)] + pub const fn with_gain(self, setting: Gain) -> AdControl { + let zeroed_setting = self.0 & !Gain::MASK; + AdControl(zeroed_setting | setting as u8) + } + + #[inline(always)] + pub const fn with_sensor_detect_current(self, setting: Sdcs) -> AdControl { + let zeroed_setting = self.0 & !Sdcs::MASK; + AdControl(zeroed_setting | setting as u8) + } + + #[inline(always)] + pub const fn with_clock_out(self, setting: ClockOut) -> AdControl { + let zeroed_setting = self.0 & !ClockOut::MASK; + AdControl(zeroed_setting | setting as u8) + } + + /// Programmable gain amplifier setting. + /// *Note: On the ADS1256 0b110 and 0b111 both represent a gain amplification setting of X64 but + /// in this driver it will always be stored as 0b110.* + #[inline] + pub const fn gain(self) -> Gain { + let masked = self.0 & Gain::MASK; + if masked == Gain::ALT_X64 { + Gain::X64 + } else { + unsafe { mem::transmute::(masked) } + } + } + + /// Sensor detect current sources. For testing that sensor is still connected and able to pass + /// current. + #[inline(always)] + pub const fn sensor_detect_current(self) -> Sdcs { + unsafe { mem::transmute::(self.0 & Sdcs::MASK) } + } + + /// Ads1256 master clock cycle frequency outputted through GPIO. + #[inline(always)] + pub const fn clock_out(self) -> ClockOut { + unsafe { mem::transmute::(self.0 & ClockOut::MASK) } + } +} + +impl Into for AdControl { + #[inline(always)] + fn into(self) -> u8 { + self.0 + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for AdControl { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + "AdControl(gain: {}, sensor_detect_current: {}, clock_out: {})", + self.gain(), + self.sensor_detect_current(), + self.clock_out() + ) + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Data Rate ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +impl DataRate { + pub const fn from_byte(byte: u8) -> DataRate { + const SPS2_5: u8 = DataRate::Sps2_5 as u8; + const SPS5: u8 = DataRate::Sps5 as u8; + const SPS10: u8 = DataRate::Sps10 as u8; + const SPS15: u8 = DataRate::Sps15 as u8; + const SPS25: u8 = DataRate::Sps25 as u8; + const SPS30: u8 = DataRate::Sps30 as u8; + const SPS50: u8 = DataRate::Sps50 as u8; + const SPS60: u8 = DataRate::Sps60 as u8; + const SPS100: u8 = DataRate::Sps100 as u8; + const SPS500: u8 = DataRate::Sps500 as u8; + const SPS1000: u8 = DataRate::Sps1000 as u8; + const SPS2000: u8 = DataRate::Sps2000 as u8; + const SPS3750: u8 = DataRate::Sps3750 as u8; + const SPS7500: u8 = DataRate::Sps7500 as u8; + const SPS15000: u8 = DataRate::Sps15000 as u8; + const SPS30000: u8 = DataRate::Sps30000 as u8; + + match byte { + SPS2_5 => DataRate::Sps2_5, + SPS5 => DataRate::Sps5, + SPS10 => DataRate::Sps10, + SPS15 => DataRate::Sps15, + SPS25 => DataRate::Sps25, + SPS30 => DataRate::Sps30, + SPS50 => DataRate::Sps50, + SPS60 => DataRate::Sps60, + SPS100 => DataRate::Sps100, + SPS500 => DataRate::Sps500, + SPS1000 => DataRate::Sps1000, + SPS2000 => DataRate::Sps2000, + SPS3750 => DataRate::Sps3750, + SPS7500 => DataRate::Sps7500, + SPS15000 => DataRate::Sps15000, + SPS30000 => DataRate::Sps30000, + _ => panic!("Invalid ADS1256 data rate."), + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- GPIO ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[derive(Copy, Clone, Eq, PartialEq)] +#[repr(transparent)] +pub struct DigitalIo(pub u8); + +#[derive(Copy, Clone, Eq, PartialEq)] +#[repr(transparent)] +pub struct DigitalIoState(pub u8); + +impl DigitalIoState { + #[inline] + pub const fn new(io0: DState, io1: DState, io2: DState, io3: DState) -> DigitalIoState { + DigitalIoState(io0 as u8 | ((io1 as u8) << 1) | ((io2 as u8) << 2) | ((io3 as u8) << 3)) + } + + #[inline(always)] + pub const fn default() -> DigitalIoState { + DigitalIoState::new(DState::Low, DState::Low, DState::Low, DState::Low) + } +} + +impl Into for DigitalIoState { + #[inline(always)] + fn into(self) -> u8 { + self.0 + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +#[repr(transparent)] +pub struct DigitalIoDirection(pub u8); + +impl DigitalIoDirection { + #[inline] + pub const fn new( + io0: DioDirection, + io1: DioDirection, + io2: DioDirection, + io3: DioDirection, + ) -> DigitalIoDirection { + DigitalIoDirection( + ((io0 as u8) << 4) | ((io1 as u8) << 5) | ((io2 as u8) << 6) | ((io3 as u8) << 7), + ) + } + + #[inline(always)] + pub const fn default() -> DigitalIoDirection { + DigitalIoDirection::new( + DioDirection::Output, + DioDirection::Input, + DioDirection::Input, + DioDirection::Input, + ) + } +} + +impl Into for DigitalIoDirection { + #[inline(always)] + fn into(self) -> u8 { + self.0 + } +} + +impl DigitalIo { + #[inline(always)] + pub const fn setting(io_state: DigitalIoState, io_direction: DigitalIoDirection) -> DigitalIo { + DigitalIo(io_state.0 | io_direction.0) + } + + #[inline(always)] + pub const fn with_io_state(self, state: DigitalIoState) -> DigitalIo { + let zeroed_setting = self.0 & !DState::ALL_MASK; + DigitalIo(zeroed_setting | state.0) + } + + #[inline(always)] + pub const fn with_io_direction(self, direction: DigitalIoDirection) -> DigitalIo { + let zeroed_setting = self.0 & !DioDirection::ALL_MASK; + DigitalIo(zeroed_setting | direction.0) + } + + #[inline(always)] + pub const fn with_io0_state(self, state: DState) -> DigitalIo { + let zeroed_setting = self.0 & !DState::IO0_MASK; + DigitalIo(zeroed_setting | state as u8) + } + + #[inline(always)] + pub const fn with_io1_state(self, state: DState) -> DigitalIo { + let zeroed_setting = self.0 & !DState::IO1_MASK; + DigitalIo(zeroed_setting | ((state as u8) << 1)) + } + + #[inline(always)] + pub const fn with_io2_state(self, state: DState) -> DigitalIo { + let zeroed_setting = self.0 & !DState::IO2_MASK; + DigitalIo(zeroed_setting | ((state as u8) << 2)) + } + + #[inline(always)] + pub const fn with_io3_state(self, state: DState) -> DigitalIo { + let zeroed_setting = self.0 & !DState::IO3_MASK; + DigitalIo(zeroed_setting | ((state as u8) << 3)) + } + + #[inline(always)] + pub const fn io0_state(self) -> DState { + match self.0 & DState::IO0_MASK { + DState::IO0_MASK => DState::High, + _ => DState::Low, + } + } + + #[inline(always)] + pub const fn io1_state(self) -> DState { + match self.0 & DState::IO1_MASK { + DState::IO1_MASK => DState::High, + _ => DState::Low, + } + } + + #[inline(always)] + pub const fn io2_state(self) -> DState { + match self.0 & DState::IO2_MASK { + DState::IO2_MASK => DState::High, + _ => DState::Low, + } + } + + #[inline(always)] + pub const fn io3_state(self) -> DState { + match self.0 & DState::IO3_MASK { + DState::IO3_MASK => DState::High, + _ => DState::Low, + } + } + + #[inline(always)] + pub const fn io0_direction(self) -> DioDirection { + match self.0 & DioDirection::IO0_MASK { + DioDirection::IO0_MASK => DioDirection::Input, + _ => DioDirection::Output, + } + } + + #[inline(always)] + pub const fn io1_direction(self) -> DioDirection { + match self.0 & DioDirection::IO1_MASK { + DioDirection::IO1_MASK => DioDirection::Input, + _ => DioDirection::Output, + } + } + + #[inline(always)] + pub const fn io2_direction(self) -> DioDirection { + match self.0 & DioDirection::IO2_MASK { + DioDirection::IO2_MASK => DioDirection::Input, + _ => DioDirection::Output, + } + } + + #[inline(always)] + pub const fn io3_direction(self) -> DioDirection { + match self.0 & DioDirection::IO3_MASK { + DioDirection::IO3_MASK => DioDirection::Input, + _ => DioDirection::Output, + } + } +} + +impl Into for DigitalIo { + #[inline(always)] + fn into(self) -> u8 { + self.0 + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for DigitalIo { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + "DigitalIo(io0: {}, {}, io1: {}, {}, io2: {}, {}, io3: {}, {})", + self.io0_direction(), + self.io0_state(), + self.io1_direction(), + self.io1_state(), + self.io2_direction(), + self.io2_state(), + self.io3_direction(), + self.io3_state() + ) + } +} diff --git a/drivers/ads1256/types/src/standard.rs b/drivers/ads1256/types/src/standard.rs new file mode 100644 index 0000000..14c480c --- /dev/null +++ b/drivers/ads1256/types/src/standard.rs @@ -0,0 +1,41 @@ +pub mod input { + use crate::{Buffer, Config, DataRate, Gain, Multiplexer, MuxInput}; + + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum Differential { + AIn0 = Multiplexer::setting(MuxInput::AIn0, MuxInput::AIn1).0, + AIn1 = Multiplexer::setting(MuxInput::AIn2, MuxInput::AIn3).0, + AIn2 = Multiplexer::setting(MuxInput::AIn4, MuxInput::AIn5).0, + AIn3 = Multiplexer::setting(MuxInput::AIn6, MuxInput::AIn7).0, + } + + impl Into for Differential { + #[inline(always)] + fn into(self) -> Multiplexer { + Multiplexer(self as u8) + } + } + + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + #[repr(u8)] + pub enum SingleEnded { + AIn0 = Multiplexer::setting(MuxInput::AIn0, MuxInput::Common).0, + AIn1 = Multiplexer::setting(MuxInput::AIn1, MuxInput::Common).0, + AIn2 = Multiplexer::setting(MuxInput::AIn2, MuxInput::Common).0, + AIn3 = Multiplexer::setting(MuxInput::AIn3, MuxInput::Common).0, + AIn4 = Multiplexer::setting(MuxInput::AIn4, MuxInput::Common).0, + AIn5 = Multiplexer::setting(MuxInput::AIn5, MuxInput::Common).0, + AIn6 = Multiplexer::setting(MuxInput::AIn6, MuxInput::Common).0, + AIn7 = Multiplexer::setting(MuxInput::AIn7, MuxInput::Common).0, + } + + impl Into for SingleEnded { + #[inline(always)] + fn into(self) -> Multiplexer { + Multiplexer(self as u8) + } + } +} diff --git a/examples/ads1256/.cargo/config.toml b/examples/ads1256/.cargo/config.toml new file mode 100644 index 0000000..4d657a2 --- /dev/null +++ b/examples/ads1256/.cargo/config.toml @@ -0,0 +1,9 @@ +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +# replace STM32F411CEUx with your chip as listed in `probe-rs chip list` +runner = "probe-rs run --chip STM32F411CEUx" + +[build] +target = "thumbv7em-none-eabi" + +[env] +DEFMT_LOG = "trace" \ No newline at end of file diff --git a/examples/ads1256/Cargo.toml b/examples/ads1256/Cargo.toml new file mode 100644 index 0000000..9adbdf9 --- /dev/null +++ b/examples/ads1256/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "ads1256-examples" +description = "Examples using the ads1256." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies.ads1256] +path = "../../drivers/ads1256/driver" +[dependencies.physical] +path = "../.." +features = ["defmt"] +[dependencies.defmt] +workspace = true +[dependencies.defmt-rtt] +workspace = true +[dependencies.cortex-m] +workspace = true +features = ["critical-section-single-core"] +[dependencies.cortex-m-rt] +workspace = true +[dependencies.embassy-stm32] +workspace = true +features = ["stm32f411ce", "memory-x", "time", "exti", "time-driver-any"] +[dependencies.embassy-executor] +workspace = true +[dependencies.embassy-futures] +workspace = true +[dependencies.embassy-time] +workspace = true +features = ["tick-hz-16_000_000"] +[dependencies.panic-probe] +workspace = true +[dependencies] +log = "0.4.20" diff --git a/examples/ads1256/build.rs b/examples/ads1256/build.rs new file mode 100644 index 0000000..56127fd --- /dev/null +++ b/examples/ads1256/build.rs @@ -0,0 +1,5 @@ +fn main() { + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); + println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); +} \ No newline at end of file diff --git a/examples/ads1256/src/bin/adc.rs b/examples/ads1256/src/bin/adc.rs new file mode 100644 index 0000000..6880e07 --- /dev/null +++ b/examples/ads1256/src/bin/adc.rs @@ -0,0 +1,131 @@ +#![no_std] +#![no_main] + +use cortex_m::prelude::{_embedded_hal_blocking_delay_DelayMs, _embedded_hal_blocking_delay_DelayUs}; +use {defmt_rtt as _, panic_probe as _}; + +use {embassy_executor as executor, embassy_stm32 as stm32}; + +use ads1256::{ + AdControl, Ads1256, AutoCal, BitOrder, Buffer, ClockOut, Config, DState, DataRate, DigitalIo, + DigitalIoDirection, DigitalIoState, DioDirection, Gain, Multiplexer, MuxInput, OutputPin, Sdcs, + SpiBus, Status, Wait, BlockingDelay +}; +use embassy_time::{Delay, Timer}; +use executor::Spawner; +use physical::quantity::Quantity; +use stm32::dma::NoDma; +use stm32::exti::ExtiInput; +use stm32::gpio::{Input, Level, Output, Pull, Speed}; +use stm32::spi::Spi; +use stm32::time::Hertz; +use stm32::{pac, spi}; + +use defmt::{debug, error, info, trace, unwrap}; + +const AUTOCAL_CONF: Config = Config { + status: Status::setting(Buffer::Enabled, AutoCal::Enabled, BitOrder::MostSigFirst), + multiplexer: Multiplexer::setting(MuxInput::AIn0, MuxInput::Common), + ad_control: AdControl::setting(Gain::X2, Sdcs::Off, ClockOut::Off), + data_rate: DataRate::Sps2_5, + digital_io: DigitalIo::setting(DigitalIoState::default(), DigitalIoDirection::default()), +}; + +const ADS1256_DELAY: ads1256::DefaultDelay = ads1256::DefaultDelay::new(Delay); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + unsafe { + pac::FLASH.acr().modify(|v| { + v.set_prften(true); + v.set_icen(true); + v.set_dcen(true); + }); + } + + let p = embassy_stm32::init(Default::default()); + + let mut spi_conf = spi::Config::default(); + spi_conf.mode = spi::MODE_1; + spi_conf.bit_order = spi::BitOrder::MsbFirst; + spi_conf.frequency = Hertz(ads1256::defaults::SPI_CLK_HZ); + + let ads1256_data_ready = ExtiInput::new(Input::new(p.PA3, Pull::Up), p.EXTI3); + let select_ads1256 = Output::new(p.PA1, Level::High, Speed::VeryHigh); + + let mut spi = Spi::new( + p.SPI1, + p.PA5, + p.PA7, + p.PA6, + NoDma, + NoDma, + spi_conf, + ); + + let mut ads_1256 = Ads1256::new(ADS1256_DELAY, select_ads1256, ads1256_data_ready); + // single_conversion(&mut spi, &mut ads_1256).await; + // ads_1256.delayer.t11_1_delay(); + // read_continuous(&mut spi, &mut ads_1256).await; + cycle_multiplexer(&mut spi, &mut ads_1256).await; +} + +async fn single_conversion( + spi: &mut impl SpiBus, + ads_1256: &mut Ads1256, +) { + ads_1256.write_config(spi, AUTOCAL_CONF).unwrap(); + ads_1256.delayer.t11_1_delay(); + ads_1256.conversion_init(spi).unwrap(); + let data = ads_1256.cmd_read_data(spi).await.unwrap(); + info!("data: {}, volts: {}", data, data.to_voltage(AUTOCAL_CONF.ad_control.gain()).fmt(Some(5))); +} + +async fn read_continuous( + spi: &mut impl SpiBus, + ads_1256: &mut Ads1256, +) { + ads_1256.write_config(spi, AUTOCAL_CONF).unwrap(); + ads_1256.delayer.t11_1_delay(); + ads_1256.start_rdatac(spi).await.unwrap(); + loop { + let data = ads_1256.read_data(spi).await.unwrap(); + info!("data: {}, volts: {}", data, data.to_voltage(AUTOCAL_CONF.ad_control.gain()).fmt(Some(5))); + } +} + +async fn cycle_multiplexer( + spi: &mut impl SpiBus, + ads_1256: &mut Ads1256, +) { + const INPUT_1: Multiplexer = Multiplexer::setting(MuxInput::AIn1, MuxInput::Common); + const INPUT_2: Multiplexer = Multiplexer::setting(MuxInput::AIn2, MuxInput::Common); + const INPUT_3: Multiplexer = Multiplexer::setting(MuxInput::AIn3, MuxInput::Common); + + let ad_control = AUTOCAL_CONF.ad_control; + let status = AUTOCAL_CONF.status; + + ads_1256.write_config(spi, AUTOCAL_CONF).unwrap(); + ads_1256.delayer.t11_1_delay(); + loop { + let ad_control = ad_control.with_gain(Gain::X4); + let data = ads_1256 + .autocal_convert(spi, INPUT_1, None, Some(ad_control.with_gain(Gain::X4)), None, false) + .await + .unwrap(); + info!("Input 1: data: {}, volts: {}", data, data.to_voltage(ad_control.gain()).fmt(Some(5))); + let ad_control = ad_control.with_gain(Gain::X8); + let data = ads_1256 + .autocal_convert(spi, INPUT_2, None, Some(ad_control.with_gain(Gain::X8)), None, false) + .await + .unwrap(); + info!("Input 2: data: {}, volts: {}", data, data.to_voltage(ad_control.gain()).fmt(Some(5))); + let ad_control = ad_control.with_gain(Gain::X16); + let data = ads_1256 + .autocal_convert(spi, INPUT_3, None, Some(ad_control.with_gain(Gain::X16)), None, false) + .await + .unwrap(); + info!("Input 3: data: {}, volts: {}", data, data.to_voltage(ad_control.gain()).fmt(Some(5))); + } + +} diff --git a/examples/ads1256/src/bin/registers.rs b/examples/ads1256/src/bin/registers.rs new file mode 100644 index 0000000..4e959b2 --- /dev/null +++ b/examples/ads1256/src/bin/registers.rs @@ -0,0 +1,180 @@ +#![no_std] +#![no_main] + +use cortex_m::prelude::_embedded_hal_blocking_delay_DelayUs; +use {defmt_rtt as _, panic_probe as _}; + +use {embassy_executor as executor, embassy_stm32 as stm32}; + +use ads1256::{ + AdControl, Ads1256, AutoCal, BitOrder, Buffer, ClockOut, Config, DState, DataRate, DigitalIo, + DigitalIoDirection, DigitalIoState, DioDirection, Gain, Multiplexer, MuxInput, OutputPin, Sdcs, + SpiBus, Status, Wait, +}; +use embassy_time::Delay; +use executor::Spawner; +use stm32::dma::NoDma; +use stm32::exti::ExtiInput; +use stm32::gpio::{Input, Level, Output, Pull, Speed}; +use stm32::spi::Spi; +use stm32::time::Hertz; +use stm32::{pac, spi}; + +use defmt::{debug, error, info, trace, unwrap}; + +const ADS1256_DELAY: ads1256::DefaultDelay = ads1256::DefaultDelay::new(Delay); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + unsafe { + pac::FLASH.acr().modify(|v| { + v.set_prften(true); + v.set_icen(true); + v.set_dcen(true); + }); + } + + let p = embassy_stm32::init(Default::default()); + + let mut spi_conf = spi::Config::default(); + spi_conf.mode = spi::MODE_1; + spi_conf.bit_order = spi::BitOrder::MsbFirst; + spi_conf.frequency = Hertz(ads1256::defaults::SPI_CLK_HZ); + + let ads1256_data_ready = ExtiInput::new(Input::new(p.PA3, Pull::Up), p.EXTI3); + let select_ads1256 = Output::new(p.PA1, Level::High, Speed::VeryHigh); + + let mut spi = Spi::new( + p.SPI1, + p.PA5, + p.PA7, + p.PA6, + NoDma, + NoDma, + spi_conf, + ); + + let mut ads_1256 = Ads1256::new(ADS1256_DELAY, select_ads1256, ads1256_data_ready); + // status(&mut spi, &mut ads_1256); + // multiplexer(&mut spi, &mut ads_1256); + // ad_control(&mut spi, &mut ads_1256); + // data_rate(&mut spi, &mut ads_1256); + // gpio(&mut spi, &mut ads_1256); + config(&mut spi, &mut ads_1256); +} + +fn status( + spi: &mut impl SpiBus, + ads_1256: &mut Ads1256, +) { + info!("Status register:"); + const STATUS_SETTING: Status = + Status::setting(Buffer::Disabled, AutoCal::Disabled, BitOrder::MostSigFirst); + ads_1256.write_status(spi, STATUS_SETTING).unwrap(); + let status = ads_1256.read_status(spi).unwrap(); + info!("ADS1256 starting status: {}", status); + let new_status_setting = status.with_buffer(Buffer::Enabled); + ads_1256.write_status(spi, new_status_setting).unwrap(); + let status = ads_1256.read_status(spi).unwrap(); + info!("ADS1256 new status: {}", status); +} + +fn multiplexer( + spi: &mut impl SpiBus, + ads_1256: &mut Ads1256, +) { + info!("Multiplexer register:"); + const MULTIPLEXER_SETTING: Multiplexer = Multiplexer::setting(MuxInput::AIn0, MuxInput::Common); + ads_1256 + .write_multiplexer(spi, MULTIPLEXER_SETTING) + .unwrap(); + let multiplexer = ads_1256.read_multiplexer(spi).unwrap(); + info!("ADS1256 starting multiplexer: {}", multiplexer); + let new_multiplexer_setting = multiplexer.with_positive(MuxInput::AIn1); + ads_1256 + .write_multiplexer(spi, new_multiplexer_setting) + .unwrap(); + let multiplexer = ads_1256.read_multiplexer(spi).unwrap(); + info!("ADS1256 new ad_control: {}", multiplexer); +} + +fn ad_control( + spi: &mut impl SpiBus, + ads_1256: &mut Ads1256, +) { + info!("AD control register:"); + const AD_CONTROL_SETTING: AdControl = AdControl::setting(Gain::X64, Sdcs::Off, ClockOut::Off); + ads_1256.write_ad_control(spi, AD_CONTROL_SETTING).unwrap(); + let ad_control = ads_1256.read_ad_control(spi).unwrap(); + info!("ADS1256 starting ad_control: {}", ad_control); + let new_ad_control_setting = ad_control.with_gain(Gain::X1); + ads_1256 + .write_ad_control(spi, new_ad_control_setting) + .unwrap(); + let ad_control = ads_1256.read_ad_control(spi).unwrap(); + info!("ADS1256 new ad_control: {}", ad_control); +} + +fn data_rate( + spi: &mut impl SpiBus, + ads_1256: &mut Ads1256, +) { + info!("Data rate register:"); + const DATA_RATE: DataRate = DataRate::Sps2_5; + ads_1256.write_data_rate(spi, DATA_RATE).unwrap(); + let data_rate = ads_1256.read_data_rate(spi).unwrap(); + info!("ADS1256 starting data rate: {}", data_rate); + ads_1256.write_data_rate(spi, DataRate::Sps60).unwrap(); + let data_rate = ads_1256.read_data_rate(spi).unwrap(); + info!("ADS1256 new data rate: {}", data_rate); +} + +fn gpio( + spi: &mut impl SpiBus, + ads_1256: &mut Ads1256, +) { + info!("GPIO register:"); + const GPIO: DigitalIo = DigitalIo::setting( + DigitalIoState::new(DState::Low, DState::Low, DState::Low, DState::Low), + DigitalIoDirection::new( + DioDirection::Output, + DioDirection::Input, + DioDirection::Input, + DioDirection::Output, + ), + ); + ads_1256.write_gpio(spi, GPIO).unwrap(); + let gpio = ads_1256.read_gpio(spi).unwrap(); + info!("ADS1256 starting gpio: {}", gpio); + let new_gpio_setting = gpio.with_io3_state(DState::High); + ads_1256.write_gpio(spi, new_gpio_setting).unwrap(); + let gpio = ads_1256.read_gpio(spi).unwrap(); + info!("ADS1256 new gpio: {}", gpio); +} + +fn config( + spi: &mut impl SpiBus, + ads_1256: &mut Ads1256, +) { + info!("Config:"); + let config = ads_1256.read_config(spi).unwrap(); + info!("ADS1256 starting config: {}", config); + const CONFIG: Config = Config { + status: Status::setting(Buffer::Enabled, AutoCal::Enabled, BitOrder::MostSigFirst), + multiplexer: Multiplexer::setting(MuxInput::AIn4, MuxInput::AIn5), + ad_control: AdControl::setting(Gain::X16, Sdcs::Off, ClockOut::Off), + data_rate: DataRate::Sps10, + digital_io: DigitalIo::setting( + DigitalIoState::new(DState::Low, DState::Low, DState::Low, DState::Low), + DigitalIoDirection::new( + DioDirection::Output, + DioDirection::Input, + DioDirection::Input, + DioDirection::Input, + ), + ), + }; + ads_1256.write_config(spi, CONFIG).unwrap(); + let config = ads_1256.read_config(spi).unwrap(); + info!("ADS1256 new config: {:#?}", config); +} diff --git a/generate-quantity/Cargo.toml b/generate-quantity/Cargo.toml new file mode 100644 index 0000000..1fedb42 --- /dev/null +++ b/generate-quantity/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "generate-quantity" +description = "Macros for generating physical quantity type" +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[lib] +proc-macro = true + +[dependencies.quote] +workspace = true +[dependencies.syn] +workspace = true +[dependencies.proc-macro2] +workspace = true \ No newline at end of file diff --git a/generate-quantity/src/lib.rs b/generate-quantity/src/lib.rs new file mode 100644 index 0000000..3b8e230 --- /dev/null +++ b/generate-quantity/src/lib.rs @@ -0,0 +1,143 @@ +extern crate proc_macro; +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::quote; +use std::ops::Deref; +use syn::parse::{Parse, ParseStream}; +use syn::{parse_macro_input, Ident, LitStr, Token}; + +const NUMBER_TYPES: &[&str] = &[ + "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", "u128", "i128", "usize", "isize", "f32", + "f64", +]; + +// Define the input structure for the macro +struct QuantityInput { + struct_name: Ident, + symbol: LitStr, +} + +// Implement the parsing for the input structure +impl Parse for QuantityInput { + fn parse(input: ParseStream) -> syn::Result { + let struct_name: Ident = input.parse()?; + input.parse::()?; + let symbol: LitStr = input.parse()?; + Ok(QuantityInput { + struct_name, + symbol, + }) + } +} + +//TODO: Remove requirement for physical::quantity::{Quantity, Value} +/// The following imports must be in scope for this macro to work +/// ``` +/// use generate_quantity::quantity_type; +/// use physical::quantity::{Quantity, Value}; +/// ``` +#[proc_macro] +pub fn quantity_type(input: TokenStream) -> TokenStream { + // Parse the input tokens into the QuantityInput structure + let QuantityInput { + struct_name, + symbol, + } = parse_macro_input!(input as QuantityInput); + + //----- Value Extension ---------------------------------- + let mut val_ext_name = String::new(); + let struct_name_str = struct_name.to_string(); + let mut struct_name_chars = struct_name_str.chars(); + let first = struct_name_chars + .next() + .expect("Struct name cannot be 0 length"); + val_ext_name.push(first.to_ascii_lowercase()); + for character in struct_name_chars { + if character.is_uppercase() { + val_ext_name.push('_'); + val_ext_name.push(character.to_ascii_lowercase()); + } else { + val_ext_name.push(character); + } + } + let val_ext_fn_name = Ident::new(val_ext_name.deref(), struct_name.span()); + let val_ext_trait_name = + Ident::new(format!("{struct_name_str}Val").deref(), struct_name.span()); + + //----- Conversion impls ---------------------------------- + let mut conversion_impls: Vec = Vec::new(); + for ¤t_type in NUMBER_TYPES { + // Generate conversion methods for all primitive types except the current_type + let conversions = NUMBER_TYPES + .iter() + .filter(|&&t| t != current_type) + .map(|&target_type| { + let method_name = Ident::new(&format!("as_{}", target_type), Span::call_site()); + let target_type = Ident::new(target_type, Span::call_site()); + + quote! { + /// Directly [as] cast this quantities value while maintaining the type of the quantity. + #[inline(always)] + pub fn #method_name(self) -> #struct_name<#target_type> { + #struct_name(self.0 as #target_type) + } + } + }) + .collect::>(); + + let current_type = Ident::new(current_type, Span::call_site()); + // Generate the impl block for the struct with the current_type + let expanded = quote! { + impl #struct_name<#current_type> { + #(#conversions)* + } + }; + + conversion_impls.push(expanded); + } + + //----- Output Code ---------------------------------- + let expanded = quote! { + #[derive( + Copy, + Clone, + PartialEq, + PartialOrd, + Debug, + derive_more::Add, + derive_more::AddAssign, + derive_more::Sub, + derive_more::SubAssign, + derive_more::Display + )] + #[display(fmt = "{} {}", _0, "Self::symbol()")] + #[repr(transparent)] + pub struct #struct_name(pub V); + + impl Quantity for #struct_name { + #[inline(always)] + fn value(self) -> V { + self.0 + } + + #[inline(always)] + fn symbol() -> &'static str { + #symbol + } + } + + pub trait #val_ext_trait_name : Value { + /// Create a quantity in the given unit from this [Value] + #[inline(always)] + fn #val_ext_fn_name(self) -> #struct_name { + #struct_name(self) + } + } + + impl #val_ext_trait_name for V {} + + #(#conversion_impls)* + }; + + expanded.into() +} diff --git a/node/Cargo.toml b/node/Cargo.toml new file mode 100644 index 0000000..47dcc77 --- /dev/null +++ b/node/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "physical-node" +description = "A node hosts peripherals." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[features] +comms = [] +single-packet-msgs = [] +usb = ["embassy-usb"] +stm32 = ["embassy-stm32", "physical/stm32"] + +[dependencies.physical] +path = ".." +[dependencies.embedded-hal] +workspace = true +[dependencies.embedded-hal-async] +workspace = true +[dependencies.defmt] +workspace = true +[dependencies.embassy-stm32] +workspace = true +optional = true +[dependencies.embassy-usb] +workspace = true +optional = true diff --git a/node/src/comms.rs b/node/src/comms.rs new file mode 100644 index 0000000..d08b29b --- /dev/null +++ b/node/src/comms.rs @@ -0,0 +1,17 @@ +pub trait Sender { + async fn send(&mut self, msg: &[u8]) -> Result<(), Reset>; +} + +pub trait Receiver { + async fn receive(&mut self, buffer: &mut [u8]) -> Result<(), Reset>; +} + +//TODO: Replace with ! +pub struct Never; + +/// Communication errors indicates either: +/// Our connection was already disconnected, in which case we should reset and wait for new connection to made. +/// or +/// There was an unexpected, irrecoverable error in communication, in which case we don't want to enter a terminal error +/// safe mode, because there is no indication the actual control is broken, so all we can really do is reset the connection. +pub struct Reset; \ No newline at end of file diff --git a/node/src/lib.rs b/node/src/lib.rs new file mode 100644 index 0000000..a876374 --- /dev/null +++ b/node/src/lib.rs @@ -0,0 +1,12 @@ +#![no_std] + +#[cfg(feature = "comms")] +pub mod comms; +pub mod spi; +#[cfg(feature = "stm32")] +pub mod stm32; + +pub use physical::CriticalError; + +pub const GPIO_ERROR_MSG: &'static str = + "Driver does not support GPIO pins with expected failure states"; diff --git a/node/src/spi.rs b/node/src/spi.rs new file mode 100644 index 0000000..efcd51b --- /dev/null +++ b/node/src/spi.rs @@ -0,0 +1,52 @@ +use crate::GPIO_ERROR_MSG; +use embedded_hal::digital::OutputPin; +use embedded_hal::spi; +use embedded_hal::spi::SpiBus; + +/// End the SPI operation if the result was an error. +/// This function will attempt to flush the SPI bus if the result being inspected was an error. +/// If the flush fails, the flush error will be ignored and the original error will be returned. +#[inline] +pub fn end_spi_if_err( + slave_select: &mut impl OutputPin, + spi: &mut SpiT, + result: Result::Error>, +) -> Result::Error> { + match result { + Ok(_) => result, + Err(_) => { + // Ignore flush error and return the original error + slave_select.set_high().expect(GPIO_ERROR_MSG); + let _ = spi.flush(); + result + }, + } +} + +/// End the series of SPI operations and forward the error of the final operation if there was +/// one. Error handling: +/// * If there was an error in the SPI operation and an error flushing the bus, returns the original +/// SPI error, not the bus error. +/// * If there was not an error in the SPI operation but was an error flushing the bus, returns the +/// bus flush error. +/// * If there was an error in the SPI operation and no error flushing the bus, returns the original +/// SPI error. +#[inline] +pub fn end_spi( + slave_select: &mut impl OutputPin, + spi: &mut SpiT, + result: Result::Error>, +) -> Result::Error> { + match result { + Ok(_) => { + slave_select.set_high().expect(GPIO_ERROR_MSG); + spi.flush()?; + result + }, + Err(_) => { + slave_select.set_high().expect(GPIO_ERROR_MSG); + let _ = spi.flush(); + result + }, + } +} diff --git a/node/src/stm32/mod.rs b/node/src/stm32/mod.rs new file mode 100644 index 0000000..5ff0b93 --- /dev/null +++ b/node/src/stm32/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "usb")] +pub mod usb; \ No newline at end of file diff --git a/node/src/stm32/usb.rs b/node/src/stm32/usb.rs new file mode 100644 index 0000000..dff3ac6 --- /dev/null +++ b/node/src/stm32/usb.rs @@ -0,0 +1,41 @@ +// The library will have build errors when built on its own (due to not having embassy-stm32 feature for a specific microcontroller) +// but it will work fine when used as a dependency for firmware that has the feature for a specific stm32 microcontroller. + +use crate::comms; +use embassy_stm32::peripherals::USB_OTG_FS; +use embassy_stm32::usb_otg::{Driver, Endpoint, In, Out}; +use embassy_usb::driver::{EndpointIn, EndpointOut}; +use embassy_usb::UsbDevice; + +pub type TypedUSB = UsbDevice<'static, Driver<'static, USB_OTG_FS>>; +pub type TypedInterIn = Endpoint<'static, USB_OTG_FS, In>; +pub type TypedInterOut = Endpoint<'static, USB_OTG_FS, Out>; + +pub struct UsbIO { + /// Send to master + pub interrupt_in: TypedInterIn, + /// Recieve from master + pub interrupt_out: TypedInterOut, +} + +impl comms::Sender for TypedInterIn { + async fn send(&mut self, msg: &[u8]) -> Result<(), comms::Reset> { + self.write(msg).await.map_err(|_| comms::Reset) + } +} + +impl comms::Receiver for TypedInterOut { + #[cfg(feature = "single-packet-msgs")] + async fn receive(&mut self, buffer: &mut [u8]) -> Result<(), comms::Reset> { + // This is OK when all our messages are smaller than a single packet. + self.read(buffer) + .await + .map(|_| ()) + .map_err(|_| comms::Reset) + } + + #[cfg(not(feature = "single-packet-msgs"))] + async fn receive(&mut self, buffer: &mut [u8]) -> Result<(), comms::Reset> { + todo!("Decide if we want a general purpose multi-packet message receive") + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..7e73c94 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,14 @@ +# Before upgrading check that everything is available on all tier1 targets here: +# https://rust-lang.github.io/rustup-components-history +[toolchain] +channel = "1.82" +components = [ "rust-src", "rustfmt", "llvm-tools" ] +targets = [ + "thumbv7em-none-eabi", + "thumbv7m-none-eabi", + "thumbv6m-none-eabi", + "thumbv7em-none-eabihf", + "thumbv8m.main-none-eabihf", + "riscv32imac-unknown-none-elf", + "wasm32-unknown-unknown", +] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..1a2c99f --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +imports_granularity = "Module" +format_strings = true +wrap_comments = true +match_block_trailing_comma = true +enum_discrim_align_threshold = 25 +fn_call_width = 100 +comment_width = 120 +single_line_if_else_max_width = 100 \ No newline at end of file diff --git a/src/adc/mod.rs b/src/adc/mod.rs new file mode 100644 index 0000000..8958f61 --- /dev/null +++ b/src/adc/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "stm32")] +pub mod stm32; \ No newline at end of file diff --git a/src/adc/stm32.rs b/src/adc/stm32.rs new file mode 100644 index 0000000..d5dccc9 --- /dev/null +++ b/src/adc/stm32.rs @@ -0,0 +1,9 @@ +use crate::quantity::{MilliVolts, Quantity}; + +pub fn reading_to_voltage( + reading: u32, + reference_voltage: MilliVolts, + v_ref_int_scale: u32, +) -> MilliVolts { + MilliVolts((reading * v_ref_int_scale / reference_voltage.value()) as u16) +} diff --git a/src/circuit/mod.rs b/src/circuit/mod.rs new file mode 100644 index 0000000..aa258b0 --- /dev/null +++ b/src/circuit/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "resistive-divider")] +pub mod resistive_divider; \ No newline at end of file diff --git a/src/circuit/resistive_divider.rs b/src/circuit/resistive_divider.rs new file mode 100644 index 0000000..482b699 --- /dev/null +++ b/src/circuit/resistive_divider.rs @@ -0,0 +1,43 @@ +use crate::quantity::{Ohms, Volts}; + +/// Given the resistance of the second resistor in a resistive voltage divider, calculate the resistance of the first resistor. +pub fn solve_r1( + voltage_src: Volts, + voltage_read: Volts, + r2: Ohms, +) -> Ohms { + Ohms(r2.0 * (voltage_src.0 / voltage_read.0 - 1.0)) +} + +/// Given the resistance of the first resistor in a resistive voltage divider, calculate the resistance of the second resistor. +pub fn solve_r2( + voltage_src: Volts, + voltage_read: Volts, + r1: Ohms, +) -> Ohms { + Ohms((r1.0 * voltage_read.0) / (voltage_src.0 - voltage_read.0)) +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Tests ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[cfg(test)] +mod tests { + use crate::quantity::{OhmsVal, VoltsVal}; + use super::*; + use float_cmp::assert_approx_eq; + + #[test] + fn solve_r1_test() { + let resistance = solve_r1(3.3.volts(), 2.0.volts(), 1_000.0.ohms()); + + assert_approx_eq!(f32, 650.0, resistance.0); + } + + #[test] + fn solve_r2_test() { + let resistance = solve_r2(3.3.volts(), 2.0.volts(), 1_000.0.ohms()); + + assert_approx_eq!(f32, 1538.462, resistance.0); + } +} diff --git a/src/control/mod.rs b/src/control/mod.rs new file mode 100644 index 0000000..2d4adbe --- /dev/null +++ b/src/control/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "pid")] +pub mod pid; \ No newline at end of file diff --git a/src/control/pid.rs b/src/control/pid.rs new file mode 100644 index 0000000..7988e58 --- /dev/null +++ b/src/control/pid.rs @@ -0,0 +1,363 @@ +//! A proportional-integral-derivative (PID) controller library. +//! +//! See [Pid] for the adjustable controller itself, as well as [ControlOutput] for the outputs and weights which you can use after setting up your controller. Follow the complete example below to setup your first controller! +//! +//! # Example +//! +//! ```rust +//! use physical::control::pid::Pid; +//! +//! // Create a new proportional-only PID controller with a setpoint of 15 +//! let mut pid = Pid::new(15.0, 100.0); +//! pid.proportional_gain = 10.0; +//! pid.proportional_limit = 100.0; +//! +//! // Input a measurement with an error of 5.0 from our setpoint +//! let output = pid.next_control_output(10.0); +//! +//! // Show that the error is correct by multiplying by our kp +//! assert_eq!(output, 50.0); // <-- +//! +//! // It won't change on repeat; the controller is proportional-only +//! let output = pid.next_control_output(10.0); +//! assert_eq!(output, 50.0); // <-- +//! +//! // Add a new integral term to the controller and input again +//! pid.integral_gain = 1.0; +//! pid.integral_limit = 100.0; +//! let output = pid.next_control_output(10.0); +//! +//! // Now that the integral makes the controller stateful, it will change +//! assert_eq!(output, 55.0); // <-- +//! +//! // Add our final derivative term and match our setpoint target +//! pid.derivative_gain = 2.0; +//! pid.derivative_limit = 100.0; +//! let output = pid.next_control_output(15.0); +//! +//! // The output will now say to go down due to the derivative +//! assert_eq!(output, -5.0); // <--//! + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// A trait for any numeric type usable in the PID controller +/// +/// This trait is automatically implemented for all types that satisfy `PartialOrd + num_traits::Signed + Copy`. This includes all of the signed float types and builtin integer except for [isize]: +/// - [i8] +/// - [i16] +/// - [i32] +/// - [i64] +/// - [i128] +/// - [f32] +/// - [f64] +/// +/// As well as any user type that matches the requirements +pub trait Number: PartialOrd + num_traits::Signed + Copy {} + +// Implement `Number` for all types that +// satisfy `PartialOrd + num_traits::Signed + Copy`. +impl Number for T {} + +/// Adjustable proportional-integral-derivative (PID) controller. +/// +/// This [`next_control_output`](Self::next_control_output) method is what's used to input new values into the controller to tell it what the current state of the system is. In the examples above it's only being used once, but realistically this will be a hot method. Please see [ControlOutput] for examples of how to handle these outputs; it's quite straight forward and mirrors the values of this structure in some ways. +/// +/// The last item of note is that the gain and limit fields can be safely modified during use. +/// +/// # Type Warning +/// [Number] is abstract and can be used with anything from a [i32] to an [i128] (as well as user-defined types). Because of this, very small types might overflow during calculation in [`next_control_output`](Self::next_control_output). You probably don't want to use [i8] or user-defined types around that size so keep that in mind when designing your controller. +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct Pid { + /// Ideal setpoint to strive for. + pub setpoint: T, + /// Defines the overall output filter limit. + pub output_limit: T, + /// Proportional gain. The proportional component is dependant only on the error. + /// It is the error * this value. + pub proportional_gain: T, + /// Integral gain. The integral component is dependent on the error and the integral term from the previous iteration. + /// It is the previous integral + (error * this value). + pub integral_gain: T, + /// Derivative gain. The derivative component is dependent on the rate of change of the measurement. + /// It is the (current measurement - previous measurement) * this value. + pub derivative_gain: T, + /// Limiter for the proportional term: `-p_limit <= P <= p_limit`. + pub proportional_limit: T, + /// Limiter for the integral term: `-i_limit <= I <= i_limit`. + pub integral_limit: T, + /// Limiter for the derivative term: `-d_limit <= D <= d_limit`. + pub derivative_limit: T, + /// Last calculated integral value if [Pid::i_gain] is used. + integral_term: T, + /// Previously found measurement whilst using the [Pid::next_control_output] method. + prev_measurement: Option, +} + +impl Pid +where + T: Number, +{ + /// Creates a new controller with the target setpoint and the output limit + pub fn new(setpoint: T, output_limit: T) -> Self { + Self { + setpoint, + output_limit, + proportional_gain: T::zero(), + integral_gain: T::zero(), + derivative_gain: T::zero(), + proportional_limit: T::zero(), + integral_limit: T::zero(), + derivative_limit: T::zero(), + integral_term: T::zero(), + prev_measurement: None, + } + } + + /// Sets the [Pid::setpoint] to target for this controller. + pub fn setpoint(&mut self, setpoint: T) -> &mut Self { + self.setpoint = setpoint; + self + } + + /// Given a new measurement, calculates the next control setting. + pub fn next_control_output(&mut self, measurement: T) -> T { + // Calculate the error between the ideal setpoint and the current + // measurement to compare against + let error = self.setpoint - measurement; + + // Calculate the proportional term and limit to it's individual limit + let p_unbounded = error * self.proportional_gain; + let p = apply_limit(self.proportional_limit, p_unbounded); + + // Mitigate output jumps when ki(t) != ki(t-1). + // While it's standard to use an error_integral that's a running sum of + // just the error (no ki), because we support ki changing dynamically, + // we store the entire term so that we don't need to remember previous + // ki values. + self.integral_term = self.integral_term + error * self.integral_gain; + + // Mitigate integral windup: Don't want to keep building up error + // beyond what i_limit will allow. + self.integral_term = apply_limit(self.integral_limit, self.integral_term); + + // Mitigate derivative kick: Use the derivative of the measurement + // rather than the derivative of the error. + let d_unbounded = -match self.prev_measurement { + Some(prev_measurement) => measurement - prev_measurement, + None => T::zero(), + } * self.derivative_gain; + self.prev_measurement = Some(measurement); + let d = apply_limit(self.derivative_limit, d_unbounded); + + // Calculate the final output by adding together the PID terms, then + // apply the final defined output limit + let output = p + self.integral_term + d; + let output = apply_limit(self.output_limit, output); + + output + } + + /// Resets the integral term back to zero, this may drastically change the + /// control output. + pub fn reset_integral_term(&mut self) { + self.integral_term = T::zero(); + } +} + +/// Saturating the input `value` according the absolute `limit` (`-abs(limit) <= output <= abs(limit)`). +fn apply_limit(limit: T, value: T) -> T { + num_traits::clamp(value, -limit.abs(), limit.abs()) +} + +#[cfg(test)] +mod tests { + use super::Pid; + + /// Proportional-only controller operation and limits + #[test] + fn proportional() { + let mut pid = Pid::new(10.0, 100.0); + pid.proportional_gain = 2.0; + pid.proportional_limit = 100.0; + pid.integral_limit = 100.0; + pid.derivative_limit = 100.0; + assert_eq!(pid.setpoint, 10.0); + + // Test simple proportional + assert_eq!(pid.next_control_output(0.0), 20.0); + + // Test proportional limit + pid.proportional_limit = 10.0; + assert_eq!(pid.next_control_output(0.0), 10.0); + } + + /// Derivative-only controller operation and limits + #[test] + fn derivative() { + let mut pid = Pid::new(10.0, 100.0); + pid.proportional_limit = 100.0; + pid.integral_limit = 100.0; + pid.derivative_limit = 100.0; + pid.derivative_gain = 2.0; + + // Test that there's no derivative since it's the first measurement + assert_eq!(pid.next_control_output(0.0), 0.0); + + // Test that there's now a derivative + assert_eq!(pid.next_control_output(5.0), -10.0); + + // Test derivative limit + pid.derivative_limit = 5.0; + assert_eq!(pid.next_control_output(10.0), -5.0); + } + + /// Integral-only controller operation and limits + #[test] + fn integral() { + let mut pid = Pid::new(10.0, 100.0); + pid.proportional_limit = 0.0; + pid.integral_gain = 2.0; + pid.integral_limit = 100.0; + pid.derivative_limit = 100.0; + + // Test basic integration + assert_eq!(pid.next_control_output(0.0), 20.0); + assert_eq!(pid.next_control_output(0.0), 40.0); + assert_eq!(pid.next_control_output(5.0), 50.0); + + // Test limit + pid.integral_limit = 50.0; + assert_eq!(pid.next_control_output(5.0), 50.0); + // Test that limit doesn't impede reversal of error integral + assert_eq!(pid.next_control_output(15.0), 40.0); + + // Test that error integral accumulates negative values + let mut pid2 = Pid::new(-10.0, 100.0); + pid2.proportional_limit = 100.0; + pid2.integral_gain = 2.0; + pid2.integral_limit = 100.0; + pid2.derivative_limit = 100.0; + + assert_eq!(pid2.next_control_output(0.0), -20.0); + assert_eq!(pid2.next_control_output(0.0), -40.0); + + pid2.integral_limit = 50.0; + assert_eq!(pid2.next_control_output(-5.0), -50.0); + // Test that limit doesn't impede reversal of error integral + assert_eq!(pid2.next_control_output(-15.0), -40.0); + } + + /// Checks that a full PID controller's limits work properly through multiple output iterations + #[test] + fn output_limit() { + let mut pid = Pid::new(10.0, 1.0); + pid.proportional_gain = 2.0; + pid.proportional_limit = 100.0; + pid.integral_limit = 100.0; + pid.derivative_limit = 100.0; + + let out = pid.next_control_output(0.0); + assert_eq!(out, 1.0); + + let out = pid.next_control_output(20.0); + assert_eq!(out, -1.0); + } + + /// Combined PID operation + #[test] + fn pid() { + let mut pid = Pid::new(10.0, 100.0); + pid.proportional_gain = 1.0; + pid.proportional_limit = 100.0; + pid.integral_gain = 0.1; + pid.integral_limit = 100.0; + pid.derivative_gain = 1.0; + pid.derivative_limit = 100.0; + + let out = pid.next_control_output(0.0); + assert_eq!(out, 11.0); + + let out = pid.next_control_output(5.0); + assert_eq!(out, 1.5); + + let out = pid.next_control_output(11.0); + assert_eq!(out, -5.6); + + let out = pid.next_control_output(10.0); + assert_eq!(out, 2.4); + } + + // NOTE: use for new test in future: /// Full PID operation with mixed float checking to make sure they're equal + /// PID operation with zero'd values, checking to see if different floats equal each other + #[test] + fn floats_zeros() { + let mut pid_f32 = Pid::new(10.0f32, 100.0); + pid_f32.proportional_limit = 100.0; + pid_f32.integral_limit = 100.0; + pid_f32.derivative_limit = 100.0; + + let mut pid_f64 = Pid::new(10.0, 100.0f64); + pid_f64.proportional_limit = 100.0; + pid_f64.integral_limit = 100.0; + pid_f64.derivative_limit = 100.0; + + for _ in 0..5 { + assert_eq!(pid_f32.next_control_output(0.0), pid_f64.next_control_output(0.0) as f32); + } + } + + // NOTE: use for new test in future: /// Full PID operation with mixed signed integer checking to make sure they're equal + /// PID operation with zero'd values, checking to see if different floats equal each other + #[test] + fn signed_integers_zeros() { + let mut pid_i8 = Pid::new(10i8, 100); + pid_i8.proportional_limit = 100; + pid_i8.integral_limit = 100; + pid_i8.derivative_limit = 100; + + let mut pid_i32 = Pid::new(10i32, 100); + pid_i32.proportional_limit = 100; + pid_i32.integral_limit = 100; + pid_i32.derivative_limit = 100; + + for _ in 0..5 { + assert_eq!(pid_i32.next_control_output(0), pid_i8.next_control_output(0i8) as i32); + } + } + + /// See if the controller can properly target to the setpoint after 2 output iterations + #[test] + fn setpoint() { + let mut pid = Pid::new(10.0, 100.0); + pid.proportional_gain = 1.0; + pid.proportional_limit = 100.0; + pid.integral_gain = 0.1; + pid.integral_limit = 100.0; + pid.derivative_gain = 1.0; + pid.derivative_limit = 100.0; + + let out = pid.next_control_output(0.0); + assert_eq!(out, 11.0); + + pid.setpoint(0.0); + + assert_eq!(pid.next_control_output(0.0), 1.0); + } + + /// Make sure negative limits don't break the controller + #[test] + fn negative_limits() { + let mut pid = Pid::new(10.0f32, -10.0); + pid.proportional_gain = 1.0; + pid.proportional_limit = -50.0; + pid.integral_gain = 1.0; + pid.integral_limit = -50.0; + pid.derivative_gain = 1.0; + pid.derivative_limit = -50.0; + + let out = pid.next_control_output(0.0); + assert_eq!(out, 10.0); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..7827c42 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,41 @@ +/// Indicates the transducer value is known to be impossible. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct InvalidValue; + +/// Indicates that the encoded data is not valid for the type. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct InvalidEncoding; +/// An error that it is likely impossible to recover from. This error should only be created in +/// situations where attempts to recover have already been attempted and have failed. Error handling +/// should consist of attempting to alert another system for maintenance and attempting to shut down +/// any systems that depend on the correct functionality of the component having errors. +/// +/// In many systems there can be a single function for handling any critical error as a critical +/// error always means everything needs to be stopped. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum CriticalError { + /// Critical communication failed and retries are either impossible or also failed. + Communication, + InvalidValue(InvalidValue), +} + +/// A state of this type may mean the program has encountered an error that prevents it from continuing to run +/// and should attempt to enter a safe terminal state. +/// e.g. Certain [Err]s +pub trait Terminal { + //TODO: Switch to using ! as the return type for the FnOnce when the feature is stabilized + fn terminal(self, terminate: impl FnOnce(E)) -> T; +} + +impl Terminal for Result { + fn terminal(self, terminate: impl FnOnce(E)) -> T { + match self { + Ok(value) => value, + Err(error) => { + //TODO: Remove panic when terminate returns -> ! + terminate(error); + panic!() + }, + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d31b4d5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,11 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod transducer; +pub mod control; +pub mod error; + +pub mod adc; +pub mod circuit; +pub mod quantity; + +pub use error::CriticalError; diff --git a/src/quantity/irradiance.rs b/src/quantity/irradiance.rs new file mode 100644 index 0000000..e40c729 --- /dev/null +++ b/src/quantity/irradiance.rs @@ -0,0 +1,5 @@ +use crate::quantity::{Quantity, Value}; +use generate_quantity::quantity_type; + +//----- Watts per Square Meter ---------------------------------- +quantity_type! {WattsPerSquareMeter, "W/m²"} diff --git a/src/quantity/mod.rs b/src/quantity/mod.rs new file mode 100644 index 0000000..1ab7f87 --- /dev/null +++ b/src/quantity/mod.rs @@ -0,0 +1,170 @@ +mod irradiance; +mod resistance; +mod temperature; +mod voltage; +mod volume; +mod volume_rate; +mod pressure; + +#[cfg(feature = "defmt")] +pub use defmt_impl::*; + +pub use irradiance::*; +pub use resistance::*; +pub use temperature::*; +pub use voltage::*; +pub use volume::*; +pub use volume_rate::*; +pub use pressure::*; + +use core::fmt::Display; +use core::marker::PhantomData; +use core::ops::{Add, Sub}; +use num_traits::{FromPrimitive, Num, NumCast}; + +const DECA: u8 = 10; +const DECI: u8 = 10; +const HECTO: u8 = 100; +const CENTI: u8 = 100; +const KILO: u16 = 1_000; +const MILLI: u16 = 1_000; +const MEGA: u32 = 1_000_000; +const MICRO: u32 = 1_000_000; +const GIGA: u32 = 1_000_000_000; +const NANO: u32 = 1_000_000_000; + +pub trait Quantity: Copy + PartialEq + PartialOrd + Add + Sub { + fn value(self) -> V; + + fn symbol() -> &'static str; + + /// Returns a wrapper that implements [Display] and [defmt::Format] for [Quantity]s with core number values. + /// + /// - `rounding` - Sets the number of decimal places to round to when formatting (currently ignored with defmt). + #[inline(always)] + fn fmt(self, rounding: Option) -> Fmt { + Fmt::new(self, rounding) + } +} + +pub trait Value: Num + Copy + PartialOrd + FromPrimitive + NumCast + Display {} +impl Value for V where V: Num + Copy + PartialOrd + FromPrimitive + NumCast + Display {} + +#[derive(Copy, Clone)] +pub struct Fmt> { + quantity: Q, + rounding: Option, + _phantom: PhantomData, +} + +impl> Fmt { + #[inline(always)] + fn new(quantity: Q, rounding: Option) -> Self { + Self { + quantity, + rounding, + _phantom: PhantomData, + } + } +} + +impl> Display for Fmt { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self.rounding { + Some(places) => { + write!(f, "{:.prec$} {}", self.quantity.value(), Q::symbol(), prec = places) + }, + None => write!(f, "{} {}", self.quantity.value(), Q::symbol()), + } + } +} + +#[cfg(feature = "defmt")] +mod defmt_impl { + use super::*; + use defmt::{write, Format, Formatter}; + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } + + impl> Format for Fmt { + fn format(&self, fmt: Formatter) { + write!(fmt, "{} {}", self.quantity.value(), Q::symbol()); + } + } +} diff --git a/src/quantity/pressure.rs b/src/quantity/pressure.rs new file mode 100644 index 0000000..e641aac --- /dev/null +++ b/src/quantity/pressure.rs @@ -0,0 +1,99 @@ +use crate::quantity::{KILO, Quantity, Value}; +use generate_quantity::quantity_type; + +const PASCALS_PER_TORR: f64 = 101325.0 / 760.0; +const KILO_PASCALS_PER_PSI: f64 = 6.894757293168364; + +//----- Pascals ---------------------------------- +quantity_type! {Pascals, "Pa"} + +impl Pascals { + #[inline] + pub fn to_kilo_pascals(self) -> KiloPascals { + let divisor = V::from_u16(KILO).unwrap(); + KiloPascals(self.0 / divisor) + } + + // Should this be in separate imple with `V` bound to `num_traits::Float`? + #[inline] + pub fn to_torr(self) -> Torr { + let divisor = V::from_f64(PASCALS_PER_TORR).unwrap(); + Torr(self.0 / divisor) + } +} + +//----- Kilopascals ---------------------------------- +quantity_type! {KiloPascals, "kPa"} + +impl KiloPascals { + #[inline] + pub fn to_pascals(self) -> Pascals { + let multiplier = V::from_u16(KILO).unwrap(); + Pascals(self.0 * multiplier) + } + + #[inline] + pub fn to_psi(self) -> Psi { + let divisor = V::from_f64(KILO_PASCALS_PER_PSI).unwrap(); + Psi(self.0 / divisor) + } +} + +//----- Torr ---------------------------------- +quantity_type! {Torr, "Torr"} + +// Should this bound `V` to `num_traits::Float`? +impl Torr { + #[inline] + pub fn to_pascals(self) -> Pascals { + let multiplier = V::from_f64(PASCALS_PER_TORR).unwrap(); + Pascals(self.0 * multiplier) + } +} + +//----- PSI ---------------------------------- +quantity_type! {Psi, "PSI"} + +impl Psi { + #[inline] + pub fn to_kilo_pascals(self) -> KiloPascals { + let multiplier = V::from_f64(KILO_PASCALS_PER_PSI).unwrap(); + KiloPascals(self.0 * multiplier) + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Tests ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[cfg(test)] +mod tests { + use super::*; + use float_cmp::assert_approx_eq; + + #[test] + fn pascals_kilo_pascals() { + let pascals: Pascals = 1_000.pascals(); + let kilo_pascals: KiloPascals = 1.kilo_pascals(); + + assert_eq!(pascals.0, kilo_pascals.to_pascals().0); + assert_eq!(kilo_pascals.0, pascals.to_kilo_pascals().0); + } + + #[test] + fn torr_pascals() { + let torr: Torr = 7.5.torr(); + let pascals: Pascals = 999.9177631578947.pascals(); + + assert_approx_eq!(f32, pascals.to_torr().0, torr.0); + assert_approx_eq!(f32, torr.to_pascals().0, pascals.0); + } + + #[test] + fn psi_kilo_pascals() { + let psi: Psi = 2.5.psi(); + let kilo_pascals: KiloPascals = 17.23689323292091.kilo_pascals(); + + assert_approx_eq!(f32, psi.to_kilo_pascals().0, kilo_pascals.0); + assert_approx_eq!(f32, kilo_pascals.to_psi().0, psi.0); + } +} diff --git a/src/quantity/resistance.rs b/src/quantity/resistance.rs new file mode 100644 index 0000000..8db7ce0 --- /dev/null +++ b/src/quantity/resistance.rs @@ -0,0 +1,5 @@ +use generate_quantity::quantity_type; +use crate::quantity::{Quantity, Value}; + +//----- Ohms ---------------------------------- +quantity_type! {Ohms, "Ω"} diff --git a/src/quantity/temperature.rs b/src/quantity/temperature.rs new file mode 100644 index 0000000..60bef1d --- /dev/null +++ b/src/quantity/temperature.rs @@ -0,0 +1,96 @@ +use generate_quantity::quantity_type; +use crate::quantity::{DECI, Quantity, Value}; + +const KELVIN_CELSIUS_OFFSET: f64 = 273.15; + +//----- Kelvins ---------------------------------- +quantity_type! {Kelvins, "K"} + +impl Kelvins { + #[inline] + pub fn to_celsius(self) -> Celsius { + //TODO: Make const + let offset = V::from_f64(KELVIN_CELSIUS_OFFSET).unwrap(); + Celsius(self.0 - offset) + } + + #[inline] + pub fn to_deci_kelvins(self) -> DeciKelvins { + let multiplier = V::from_u8(DECI).unwrap(); + DeciKelvins(self.0 * multiplier) + } +} + +//----- Decikelvins ---------------------------------- +quantity_type! {DeciKelvins, "dK"} + +impl DeciKelvins { + #[inline] + pub fn to_kelvins(self) -> Kelvins { + let divisor = V::from_u8(DECI).unwrap(); + Kelvins(self.0 / divisor) + } +} + +//----- Degrees Celsius ---------------------------------- +quantity_type! {Celsius, "℃"} + +impl Celsius { + #[inline] + pub fn to_kelvins(self) -> Kelvins { + //TODO: Make const + let offset = V::from_f64(KELVIN_CELSIUS_OFFSET).unwrap(); + Kelvins(self.0 + offset) + } + + #[inline] + pub fn to_deci_celsius(self) -> DeciCelsius { + let multiplier = V::from_u8(DECI).unwrap(); + DeciCelsius(self.0 * multiplier) + } +} + +//----- Deci Degrees Celsius ---------------------------------- +quantity_type! {DeciCelsius, "d℃"} + +impl DeciCelsius { + #[inline] + pub fn to_celsius(self) -> Celsius { + let divisor = V::from_u8(DECI).unwrap(); + Celsius(self.0 / divisor) + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Tests ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[cfg(test)] +mod tests { + use float_cmp::assert_approx_eq; + use super::*; + + #[test] + fn convert_f32() { + let kelvins: Kelvins = 298.15.kelvins(); + let deci_kelvins: DeciKelvins = 2_981.5.deci_kelvins(); + let celsius: Celsius = 25.0.celsius(); + let deci_celsius: DeciCelsius = 250.0.deci_celsius(); + + assert_approx_eq!(f32, kelvins.to_celsius().0, celsius.0); + assert_approx_eq!(f32, celsius.to_kelvins().0, kelvins.0); + + assert_approx_eq!(f32, deci_kelvins.to_kelvins().0, kelvins.0); + assert_approx_eq!(f32, kelvins.to_deci_kelvins().0, deci_kelvins.0); + + assert_approx_eq!(f32, deci_celsius.to_celsius().0, celsius.0); + assert_approx_eq!(f32, celsius.to_deci_celsius().0, deci_celsius.0); + } + + #[test] + fn convert_u16() { + let kelvins: Kelvins = 298.kelvins(); + let celsius: Celsius = 25.celsius(); + + assert_eq!(celsius.0, kelvins.to_celsius().0); + } +} diff --git a/src/quantity/voltage.rs b/src/quantity/voltage.rs new file mode 100644 index 0000000..f086d49 --- /dev/null +++ b/src/quantity/voltage.rs @@ -0,0 +1,52 @@ +use generate_quantity::quantity_type; +use crate::quantity::{Quantity, Value}; + +const VOLT_MV_RATIO: u16 = 1_000; + +//----- Volts ---------------------------------- +quantity_type! {Volts, "V"} + +impl Volts { + #[inline] + pub fn to_milli_volts(self) -> MilliVolts { + let multiplier = V::from_u16(VOLT_MV_RATIO).unwrap(); + MilliVolts(self.0 * multiplier) + } +} + +//----- Millivolts ---------------------------------- +quantity_type! {MilliVolts, "mV"} + +impl MilliVolts { + pub fn to_volts(self) -> Volts { + let divisor = V::from_u16(VOLT_MV_RATIO).unwrap(); + Volts(self.0 / divisor) + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Tests ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[cfg(test)] +mod tests { + use float_cmp::assert_approx_eq; + use super::*; + + #[test] + fn convert_u32() { + let volts: Volts = 3.volts(); + let millivolts: MilliVolts = 3_000.milli_volts(); + + assert_eq!(volts.to_milli_volts().0, millivolts.0); + assert_eq!(millivolts.to_volts().0, volts.0); + } + + #[test] + fn convert_f64() { + let volts: Volts = 3.0.volts(); + let millivolts: MilliVolts = 3_000.0.milli_volts(); + + assert_approx_eq!(f64, volts.to_milli_volts().0, millivolts.0); + assert_approx_eq!(f64, millivolts.to_volts().0, volts.0); + } +} diff --git a/src/quantity/volume.rs b/src/quantity/volume.rs new file mode 100644 index 0000000..a482487 --- /dev/null +++ b/src/quantity/volume.rs @@ -0,0 +1,43 @@ +use crate::quantity::{Quantity, Value}; +use generate_quantity::quantity_type; + +use crate::quantity::MILLI; + +//----- Liters ---------------------------------- +quantity_type! {Liters, "L"} + +impl Liters { + #[inline] + pub fn to_milli_liters(self) -> MilliLiters { + let multiplier = V::from_u16(MILLI).unwrap(); + MilliLiters(self.0 * multiplier) + } +} + +//----- Milliliters ---------------------------------- +quantity_type! {MilliLiters, "mL"} + +impl MilliLiters { + #[inline] + pub fn to_liters(self) -> MilliLiters { + let divisor = V::from_u16(MILLI).unwrap(); + MilliLiters(self.0 / divisor) + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Tests ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn convert_u32() { + let liters: Liters = 12.liters(); + let milli_liters: MilliLiters = 12_000.milli_liters(); + + assert_eq!(liters.0, milli_liters.to_liters().0); + assert_eq!(milli_liters.0, liters.to_milli_liters().0); + } +} diff --git a/src/quantity/volume_rate.rs b/src/quantity/volume_rate.rs new file mode 100644 index 0000000..3c79402 --- /dev/null +++ b/src/quantity/volume_rate.rs @@ -0,0 +1,5 @@ +use crate::quantity::{Quantity, Value}; +use generate_quantity::quantity_type; + +//----- Liters per Minute ---------------------------------- +quantity_type! {LitersPerMinute, "L/min"} diff --git a/src/transducer/mod.rs b/src/transducer/mod.rs new file mode 100644 index 0000000..9ee987b --- /dev/null +++ b/src/transducer/mod.rs @@ -0,0 +1,3 @@ +mod part; + +pub use part::*; \ No newline at end of file diff --git a/src/transducer/part/lm35.rs b/src/transducer/part/lm35.rs new file mode 100644 index 0000000..631543e --- /dev/null +++ b/src/transducer/part/lm35.rs @@ -0,0 +1,16 @@ +use crate::error::InvalidValue; +use crate::quantity::{DeciCelsius, MilliVolts, Quantity}; + +#[inline] +pub fn convert( + voltage: MilliVolts, +) -> Result, InvalidValue> { + const MIN_VOLTAGE: MilliVolts = MilliVolts(-550); + const MAX_VOLTAGE: MilliVolts = MilliVolts(1_500); + + if voltage >= MIN_VOLTAGE && voltage <= MAX_VOLTAGE { + Ok(DeciCelsius(voltage.value())) + } else { + Err(InvalidValue) + } +} diff --git a/src/transducer/part/mod.rs b/src/transducer/part/mod.rs new file mode 100644 index 0000000..0485ee0 --- /dev/null +++ b/src/transducer/part/mod.rs @@ -0,0 +1,10 @@ +mod thermocouple; + +#[cfg(feature = "lm35")] +pub mod lm35; + +#[cfg(feature = "thermistor")] +pub mod thermistor; + +#[cfg(feature = "thermocouple-k")] +pub use thermocouple::type_k as thermocouple_k; \ No newline at end of file diff --git a/src/transducer/part/thermistor.rs b/src/transducer/part/thermistor.rs new file mode 100644 index 0000000..5e57e39 --- /dev/null +++ b/src/transducer/part/thermistor.rs @@ -0,0 +1,38 @@ +use crate::quantity::{Kelvins, Ohms}; +use libm::{log, logf}; + +/// Convert thermistor resistance to a temperature using beta parameter equation +pub fn convert_beta( + resistance: Ohms, + beta: f32, + reference_temp: Kelvins, + reference_resist: Ohms, +) -> Kelvins { + let kelvins = 1.0 / ((logf(resistance.0 / reference_resist.0) / beta) + 1.0 / reference_temp.0); + Kelvins(kelvins) +} + +/// Convert thermistor resistance to a temperature using Steinhart-Hart equation +pub fn convert_steinhart(resistance: Ohms, a: f64, b: f64, c: f64) -> Kelvins { + let log_omhs = log(resistance.0); + let kelvins = 1.0 / (a + b * log_omhs + c * log_omhs * log_omhs * log_omhs); + Kelvins(kelvins as f32) +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Tests ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +#[cfg(test)] +mod tests { + use float_cmp::assert_approx_eq; + + use crate::quantity::{OhmsVal, KelvinsVal}; + use super::*; + + #[test] + fn convert_beta_test() { + let temperature = convert_beta(1538.462.ohms(), 3950.0, 298.15.kelvins(), 100_000.0.ohms()); + + assert_approx_eq!(f32, 435.31073, temperature.0); + } +} diff --git a/src/transducer/part/thermocouple/mod.rs b/src/transducer/part/thermocouple/mod.rs new file mode 100644 index 0000000..108a162 --- /dev/null +++ b/src/transducer/part/thermocouple/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "thermocouple-k")] +pub mod type_k; \ No newline at end of file diff --git a/src/transducer/part/thermocouple/type_k.rs b/src/transducer/part/thermocouple/type_k.rs new file mode 100644 index 0000000..778c377 --- /dev/null +++ b/src/transducer/part/thermocouple/type_k.rs @@ -0,0 +1,169 @@ +//! Note - Thermocouple conversion uses [f64] arithmetic internally. + +use libm::pow; +use crate::error::InvalidValue; +use crate::quantity::{Celsius, MilliVolts, Quantity}; + +fn _convert( + voltage: MilliVolts, +) -> Result, InvalidValue> { + let mv = voltage.value(); + let mv_pow2 = mv * mv; + let mv_pow3 = mv_pow2 * mv; + let mv_pow4 = mv_pow3 * mv; + let mv_pow5 = mv_pow4 * mv; + let mv_pow6 = mv_pow5 * mv; + + if mv >= -5.891 && mv <= 0.0 { + let mv_pow7 = mv_pow6 * mv; + let mv_pow8 = mv_pow7 * mv; + + let celsius = 2.5173462E+1 * mv + + -1.1662878 * mv_pow2 + + -1.0833638 * mv_pow3 + + -8.9773540E-1 * mv_pow4 + + -3.7342377E-1 * mv_pow5 + + -8.6632643E-2 * mv_pow6 + + -1.0450598E-2 * mv_pow7 + + -5.1920577E-4 * mv_pow8; + + Ok(Celsius(celsius as f32)) + } else if mv > 0.0 && mv < 20.644 { + let mv_pow7 = mv_pow6 * mv; + let mv_pow8 = mv_pow7 * mv; + let mv_pow9 = mv_pow8 * mv; + + let celsius = 2.508355E+1 * mv + + 7.860106E-2 * mv_pow2 + + -2.503131E-1 * mv_pow3 + + 8.315270E-2 * mv_pow4 + + -1.228034E-2 * mv_pow5 + + 9.804036E-4 * mv_pow6 + + -4.413030E-5 * mv_pow7 + + 1.057734E-6 * mv_pow8 + + -1.052755E-8 * mv_pow9; + + Ok(Celsius(celsius as f32)) + } else if mv >= 20.644 && mv <= 54.886 { + let celsius = 1.318058e2 + + 4.830222E+1 * mv + + -1.646031 * mv_pow2 + + 5.464731E-2 * mv_pow3 + + -9.650715E-4 * mv_pow4 + + 8.802193E-6 * mv_pow5 + + -3.110810E-8 * mv_pow6; + + Ok(Celsius(celsius as f32)) + } else { + Err(InvalidValue) + } +} + +/// Convert from a voltage produced by a type k thermocouple to a temperature using polynomial and +/// directly adding the reference junction temperature to the result for offset compensation. +/// +/// Can be useful compared to [convert_seebeck] when the reference temperature or the temperature +/// being read by the thermocouple is fairly close to 0. +/// +/// This function uses the [NIST type K thermocouple linearisation polynomial](https://srdata.nist.gov/its90/type_k/kcoefficients_inverse.html). +#[inline] +pub fn convert_direct( + voltage: MilliVolts, + r_junction: Celsius, +) -> Result, InvalidValue> { + let base_temp = _convert(voltage)?; + + Ok(base_temp + r_junction) +} + +/// Convert from a voltage produced by a type k thermocouple to a temperature using polynomial and +/// using a constant seebeck coefficient to correct the input voltage for offset compensation. +/// +/// Probably the right choice most of the time. +/// +/// This function uses the [NIST type K thermocouple linearisation polynomial](https://srdata.nist.gov/its90/type_k/kcoefficients_inverse.html). +#[inline] +pub fn convert_seebeck( + voltage: MilliVolts, + r_junction: Celsius, +) -> Result, InvalidValue> { + let voltage_correction = temp_to_voltage_seebeck(r_junction)?; + _convert(MilliVolts(voltage.0 + voltage_correction.0 as f64)) +} + +/// Convert from a voltage produced by a type k thermocouple to a temperature using polynomial and +/// using a polynomial to correct the input voltage for offset compensation. +/// +/// This is the most accurate method but uses the most processor cycles by a wide margin. +/// +/// This function uses the [NIST type K thermocouple linearisation polynomial](https://srdata.nist.gov/its90/type_k/kcoefficients_inverse.html). +#[inline] +pub fn convert_polynomial( + voltage: MilliVolts, + r_junction: Celsius, +) -> Result, InvalidValue> { + let voltage_correction = temp_to_voltage_poly(r_junction)?; + _convert(MilliVolts(voltage.0 + voltage_correction.0 as f64)) +} + +pub fn temp_to_voltage_poly( + temperature: Celsius, +) -> Result, InvalidValue> { + let celsius = temperature.value(); + let cel_pow2 = celsius * celsius; + let cel_pow3 = cel_pow2 * celsius; + let cel_pow4 = cel_pow3 * celsius; + let cel_pow5 = cel_pow4 * celsius; + let cel_pow6 = cel_pow5 * celsius; + let cel_pow7 = cel_pow6 * celsius; + let cel_pow8 = cel_pow7 * celsius; + let cel_pow9 = cel_pow8 * celsius; + + if celsius >= -270.0 && celsius < 0.0 { + let cel_pow10 = cel_pow9 * celsius; + + let mv = 0.394501280250E-01 * celsius + + 0.236223735980E-04 * cel_pow2 + + -0.328589067840E-06 * cel_pow3 + + -0.499048287770E-08 * cel_pow4 + + -0.675090591730E-10 * cel_pow5 + + -0.574103274280E-12 * cel_pow6 + + -0.310888728940E-14 * cel_pow7 + + -0.104516093650E-16 * cel_pow8 + + -0.198892668780E-19 * cel_pow9 + + -0.163226974860E-22 * cel_pow10; + + Ok(MilliVolts(mv as f32)) + } else if celsius >= 0.0 && celsius <= 1372.0 { + let base = celsius - 0.126968600000E+03; + let exp = -0.118343200000E-03 * (base * base); + let addition = pow(0.1185976, exp); + + let mv = -0.176004136860E-01 + + 0.389212049750E-01 * celsius + + 0.185587700320E-04 * cel_pow2 + + -0.994575928740E-07 * cel_pow3 + + 0.318409457190E-09 * cel_pow4 + + -0.560728448890E-12 * cel_pow5 + + 0.560750590590E-15 * cel_pow6 + + -0.320207200030E-18 * cel_pow7 + + 0.971511471520E-22 * cel_pow8 + + -0.121047212750E-25 * cel_pow9 + + addition; + + Ok(MilliVolts(mv as f32)) + } else { + Err(InvalidValue) + } +} + +#[inline] +pub fn temp_to_voltage_seebeck( + temperature: Celsius, +) -> Result, InvalidValue> { + if temperature.value() >= -2.0 && temperature.value() <= 800.0 { + Ok(MilliVolts(0.041 * temperature.value())) + } else { + Err(InvalidValue) + } +}