From 383ebcd8e4dd88cab535828c21161905ad8bf1e1 Mon Sep 17 00:00:00 2001 From: Astro Date: Sat, 12 Dec 2020 01:25:07 +0100 Subject: [PATCH] rewrite config for sfkv-based flash_store --- Cargo.lock | 13 ++- Cargo.toml | 2 +- README.md | 62 +++++++------- src/command_parser.rs | 60 +++++++++++-- src/config.rs | 194 +++--------------------------------------- src/flash_store.rs | 45 ++++++++++ src/main.rs | 63 ++++++++------ src/steinhart_hart.rs | 10 +-- 8 files changed, 197 insertions(+), 252 deletions(-) create mode 100644 src/flash_store.rs diff --git a/Cargo.lock b/Cargo.lock index 0720694..59a8aa8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,6 +391,15 @@ dependencies = [ "syn", ] +[[package]] +name = "sfkv" +version = "0.1.0" +dependencies = [ + "byteorder", + "postcard", + "serde", +] + [[package]] name = "smoltcp" version = "0.6.0" @@ -436,7 +445,6 @@ dependencies = [ [[package]] name = "stm32f4xx-hal" version = "0.8.3" -source = "git+https://github.com/stm32-rs/stm32f4xx-hal.git#e80925770d2fe72f0f01a7b46147f4e31d512689" dependencies = [ "bare-metal 0.2.5", "cast", @@ -444,6 +452,7 @@ dependencies = [ "cortex-m-rt", "embedded-dma", "embedded-hal", + "log", "nb 0.1.3", "rand_core", "stm32f4", @@ -491,9 +500,9 @@ dependencies = [ "num-traits", "panic-abort", "panic-semihosting", - "postcard", "serde", "serde-json-core", + "sfkv", "smoltcp", "stm32-eth", "stm32f4xx-hal", diff --git a/Cargo.toml b/Cargo.toml index c85c3fc..e75b1f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,9 +34,9 @@ nb = "1" uom = { version = "0.30", default-features = false, features = ["autoconvert", "si", "f64", "use_serde"] } eeprom24x = "0.3" serde = { version = "1.0", default-features = false, features = ["derive"] } -postcard = "0.5" heapless = "0.5" serde-json-core = "0.1" +sfkv = "0.1" [patch.crates-io] stm32f4xx-hal = { git = "https://github.com/stm32-rs/stm32f4xx-hal.git" } diff --git a/README.md b/README.md index 0da8110..9acdef9 100644 --- a/README.md +++ b/README.md @@ -91,37 +91,37 @@ The scope of this setting is per TCP session. ### Commands -| Syntax | Function | -| --- | --- | -| `report` | Show current input | -| `report mode` | Show current report mode | -| `report mode ` | Set report mode | -| `pwm` | Show current PWM settings | -| `pwm <0/1> max_i_pos ` | Set PWM duty cycle for **max_i_pos** to *ampere* | -| `pwm <0/1> max_i_neg ` | Set PWM duty cycle for **max_i_neg** to *ampere* | -| `pwm <0/1> max_v ` | Set PWM duty cycle for **max_v** to *volt* | -| `pwm <0/1> i_set ` | Disengage PID, set **i_set** DAC to *ampere* | -| `pwm <0/1> pid` | Set PWM to be controlled by PID | -| `center <0/1> ` | Set the MAX1968 0A-centerpoint to *volts* | -| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF | -| `pid` | Show PID configuration | -| `pid <0/1> target ` | Set the PID controller target temperature | -| `pid <0/1> kp ` | Set proportional gain | -| `pid <0/1> ki ` | Set integral gain (unit: 10 Hz) | -| `pid <0/1> kd ` | Set differential gain (unit: 0.1 seconds) | -| `pid <0/1> output_min ` | Set mininum output | -| `pid <0/1> output_max ` | Set maximum output | -| `pid <0/1> integral_min ` | Set integral lower bound | -| `pid <0/1> integral_max ` | Set integral upper bound | -| `s-h` | Show Steinhart-Hart equation parameters | -| `s-h <0/1> ` | Set Steinhart-Hart parameter for a channel | -| `postfilter` | Show postfilter settings | -| `postfilter <0/1> off` | Disable postfilter | -| `postfilter <0/1> rate ` | Set postfilter output data rate | -| `load` | Restore configuration from EEPROM | -| `save` | Save configuration to EEPROM | -| `reset` | Reset the device | -| `ipv4 ` | Configure IPv4 address | +| Syntax | Function | +| --- | --- | +| `report` | Show current input | +| `report mode` | Show current report mode | +| `report mode ` | Set report mode | +| `pwm` | Show current PWM settings | +| `pwm <0/1> max_i_pos ` | Set PWM duty cycle for **max_i_pos** to *ampere* | +| `pwm <0/1> max_i_neg ` | Set PWM duty cycle for **max_i_neg** to *ampere* | +| `pwm <0/1> max_v ` | Set PWM duty cycle for **max_v** to *volt* | +| `pwm <0/1> i_set ` | Disengage PID, set **i_set** DAC to *ampere* | +| `pwm <0/1> pid` | Set PWM to be controlled by PID | +| `center <0/1> ` | Set the MAX1968 0A-centerpoint to *volts* | +| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF | +| `pid` | Show PID configuration | +| `pid <0/1> target ` | Set the PID controller target temperature | +| `pid <0/1> kp ` | Set proportional gain | +| `pid <0/1> ki ` | Set integral gain (unit: 10 Hz) | +| `pid <0/1> kd ` | Set differential gain (unit: 0.1 seconds) | +| `pid <0/1> output_min ` | Set mininum output | +| `pid <0/1> output_max ` | Set maximum output | +| `pid <0/1> integral_min ` | Set integral lower bound | +| `pid <0/1> integral_max ` | Set integral upper bound | +| `s-h` | Show Steinhart-Hart equation parameters | +| `s-h <0/1> ` | Set Steinhart-Hart parameter for a channel | +| `postfilter` | Show postfilter settings | +| `postfilter <0/1> off` | Disable postfilter | +| `postfilter <0/1> rate ` | Set postfilter output data rate | +| `load [0/1]` | Restore configuration for channel all/0/1 from flash | +| `save [0/1]` | Save configuration for channel all/0/1 to flash | +| `reset` | Reset the device | +| `ipv4 [Y.Y.Y.Y]` | Configure IPv4 address, netmask length, and optional default gateway | ## USB diff --git a/src/command_parser.rs b/src/command_parser.rs index a71ddc4..6a1f649 100644 --- a/src/command_parser.rs +++ b/src/command_parser.rs @@ -132,8 +132,12 @@ pub enum CenterPoint { #[derive(Debug, Clone, PartialEq)] pub enum Command { Quit, - Load, - Save, + Load { + channel: Option, + }, + Save { + channel: Option, + }, Reset, Ipv4([u8; 4]), Show(ShowCommand), @@ -437,6 +441,38 @@ fn postfilter(input: &[u8]) -> IResult<&[u8], Result> { ))(input) } +fn load(input: &[u8]) -> IResult<&[u8], Result> { + let (input, _) = tag("load")(input)?; + let (input, channel) = alt(( + |input| { + let (input, _) = whitespace(input)?; + let (input, channel) = channel(input)?; + let (input, _) = end(input)?; + Ok((input, Some(channel))) + }, + value(None, end) + ))(input)?; + + let result = Ok(Command::Load { channel }); + Ok((input, result)) +} + +fn save(input: &[u8]) -> IResult<&[u8], Result> { + let (input, _) = tag("save")(input)?; + let (input, channel) = alt(( + |input| { + let (input, _) = whitespace(input)?; + let (input, channel) = channel(input)?; + let (input, _) = end(input)?; + Ok((input, Some(channel))) + }, + value(None, end) + ))(input)?; + + let result = Ok(Command::Save { channel }); + Ok((input, result)) +} + fn ipv4(input: &[u8]) -> IResult<&[u8], Result> { let (input, _) = tag("ipv4")(input)?; let (input, _) = whitespace(input)?; @@ -457,8 +493,8 @@ fn ipv4(input: &[u8]) -> IResult<&[u8], Result> { fn command(input: &[u8]) -> IResult<&[u8], Result> { alt((value(Ok(Command::Quit), tag("quit")), - value(Ok(Command::Load), tag("load")), - value(Ok(Command::Save), tag("save")), + load, + save, value(Ok(Command::Reset), tag("reset")), ipv4, map(report, Ok), @@ -496,13 +532,25 @@ mod test { #[test] fn parse_load() { let command = Command::parse(b"load"); - assert_eq!(command, Ok(Command::Load)); + assert_eq!(command, Ok(Command::Load { channel: None })); + } + + #[test] + fn parse_load_channel() { + let command = Command::parse(b"load 0"); + assert_eq!(command, Ok(Command::Load { channel: Some(0) })); } #[test] fn parse_save() { let command = Command::parse(b"save"); - assert_eq!(command, Ok(Command::Save)); + assert_eq!(command, Ok(Command::Save { channel: None })); + } + + #[test] + fn parse_save_channel() { + let command = Command::parse(b"save 0"); + assert_eq!(command, Ok(Command::Save { channel: Some(0) })); } #[test] diff --git a/src/config.rs b/src/config.rs index 4e39bd1..9c77b2f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,102 +1,24 @@ -use postcard::{from_bytes, to_slice}; use serde::{Serialize, Deserialize}; use smoltcp::wire::Ipv4Address; -use stm32f4xx_hal::i2c; use uom::si::{ electric_potential::volt, electric_current::ampere, - electrical_resistance::ohm, - f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature}, - thermodynamic_temperature::degree_celsius, + f64::{ElectricCurrent, ElectricPotential}, }; use crate::{ ad7172::PostFilter, - channels::{CHANNELS, Channels}, + channels::Channels, command_parser::CenterPoint, - EEPROM_SIZE, EEPROM_PAGE_SIZE, pid, - pins, steinhart_hart, }; -#[derive(Debug)] -pub enum Error { - Eeprom(eeprom24x::Error), - Encode(postcard::Error), -} - -impl From> for Error { - fn from(e: eeprom24x::Error) -> Self { - Error::Eeprom(e) - } -} - -impl From for Error { - fn from(e: postcard::Error) -> Self { - Error::Encode(e) - } -} - -/// Just for encoding/decoding, actual state resides in ChannelState -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct Config { - channels: [ChannelConfig; CHANNELS], - pub ipv4_address: [u8; 4], -} - -impl Config { - pub fn new(channels: &mut Channels, ipv4_address: Ipv4Address) -> Self { - Config { - channels: [ - ChannelConfig::new(channels, 0), - ChannelConfig::new(channels, 1), - ], - ipv4_address: ipv4_address.0, - } - } - - /// apply loaded config to system - pub fn apply(&self, channels: &mut Channels) { - for i in 0..CHANNELS { - self.channels[i].apply(channels, i); - } - } - - pub fn load(eeprom: &mut pins::Eeprom) -> Result { - let mut buffer = [0; EEPROM_SIZE]; - eeprom.read_data(0, &mut buffer)?; - log::info!("load: {:?}", buffer); - let config = from_bytes(&mut buffer)?; - Ok(config) - } - - pub fn save(&self, eeprom: &mut pins::Eeprom) -> Result<(), Error> { - let mut buffer = [0; EEPROM_SIZE]; - let config_buffer = to_slice(self, &mut buffer)?; - log::info!("save: {:?}", config_buffer); - - let mut addr = 0; - for chunk in config_buffer.chunks(EEPROM_PAGE_SIZE) { - 'write_retry: loop { - match eeprom.write_page(addr, chunk) { - Ok(()) => break 'write_retry, - Err(eeprom24x::Error::I2C(i2c::Error::NACK)) => {}, - Err(e) => Err(e)?, - } - } - addr += chunk.len() as u32; - } - - Ok(()) - } -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ChannelConfig { center: CenterPoint, pid: pid::Parameters, pid_target: f32, - sh: SteinhartHartConfig, + sh: steinhart_hart::Parameters, pwm: PwmLimits, /// uses variant `PostFilter::Invalid` instead of `None` to save space adc_postfilter: PostFilter, @@ -115,7 +37,7 @@ impl ChannelConfig { center: state.center.clone(), pid: state.pid.parameters.clone(), pid_target: state.pid.target as f32, - sh: (&state.sh).into(), + sh: state.sh.clone(), pwm, adc_postfilter, } @@ -126,7 +48,7 @@ impl ChannelConfig { state.center = self.center.clone(); state.pid.parameters = self.pid.clone(); state.pid.target = self.pid_target.into(); - state.sh = (&self.sh).into(); + state.sh = self.sh.clone(); self.pwm.apply(channels, channel); @@ -138,38 +60,11 @@ impl ChannelConfig { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -struct SteinhartHartConfig { - t0: f32, - r0: f32, - b: f32, -} - -impl From<&steinhart_hart::Parameters> for SteinhartHartConfig { - fn from(sh: &steinhart_hart::Parameters) -> Self { - SteinhartHartConfig { - t0: sh.t0.get::() as f32, - r0: sh.r0.get::() as f32, - b: sh.b as f32, - } - } -} - -impl Into for &SteinhartHartConfig { - fn into(self) -> steinhart_hart::Parameters { - steinhart_hart::Parameters { - t0: ThermodynamicTemperature::new::(self.t0.into()), - r0: ElectricalResistance::new::(self.r0.into()), - b: self.b.into(), - } - } -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] struct PwmLimits { - max_v: f32, - max_i_pos: f32, - max_i_neg: f32, + max_v: f64, + max_i_pos: f64, + max_i_neg: f64, } impl PwmLimits { @@ -178,76 +73,15 @@ impl PwmLimits { let (max_i_pos, _) = channels.get_max_i_pos(channel); let (max_i_neg, _) = channels.get_max_i_neg(channel); PwmLimits { - max_v: max_v.get::() as f32, - max_i_pos: max_i_pos.get::() as f32, - max_i_neg: max_i_neg.get::() as f32, + max_v: max_v.get::(), + max_i_pos: max_i_pos.get::(), + max_i_neg: max_i_neg.get::(), } } pub fn apply(&self, channels: &mut Channels, channel: usize) { - channels.set_max_v(channel, ElectricPotential::new::(self.max_v.into())); - channels.set_max_i_pos(channel, ElectricCurrent::new::(self.max_i_pos.into())); - channels.set_max_i_neg(channel, ElectricCurrent::new::(self.max_i_neg.into())); - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::DEFAULT_IPV4_ADDRESS; - - #[test] - fn test_fit_eeprom() { - let channel_config = ChannelConfig { - center: CenterPoint::Override(1.5), - pid: pid::Parameters::default(), - pid_target: 93.7, - sh: (&steinhart_hart::Parameters::default()).into(), - pwm: PwmLimits { - max_v: 1.65, - max_i_pos: 2.1, - max_i_neg: 2.25, - }, - adc_postfilter: PostFilter::F21SPS, - }; - let config = Config { - channels: [ - channel_config.clone(), - channel_config.clone(), - ], - ipv4_address: DEFAULT_IPV4_ADDRESS.0, - }; - - let mut buffer = [0; EEPROM_SIZE]; - let buffer = to_slice(&config, &mut buffer).unwrap(); - assert!(buffer.len() <= EEPROM_SIZE); - } - - #[test] - fn test_encode_decode() { - let channel_config = ChannelConfig { - center: CenterPoint::Override(1.5), - pid: pid::Parameters::default(), - pid_target: 93.7, - sh: (&steinhart_hart::Parameters::default()).into(), - pwm: PwmLimits { - max_v: 1.65, - max_i_pos: 2.1, - max_i_neg: 2.25, - }, - adc_postfilter: PostFilter::F21SPS, - }; - let config = Config { - channels: [ - channel_config.clone(), - channel_config.clone(), - ], - ipv4_address: DEFAULT_IPV4_ADDRESS.0, - }; - - let mut buffer = [0; EEPROM_SIZE]; - to_slice(&config, &mut buffer).unwrap(); - let decoded: Config = from_bytes(&buffer).unwrap(); - assert_eq!(decoded, config); + channels.set_max_v(channel, ElectricPotential::new::(self.max_v)); + channels.set_max_i_pos(channel, ElectricCurrent::new::(self.max_i_pos)); + channels.set_max_i_neg(channel, ElectricCurrent::new::(self.max_i_neg)); } } diff --git a/src/flash_store.rs b/src/flash_store.rs new file mode 100644 index 0000000..100786c --- /dev/null +++ b/src/flash_store.rs @@ -0,0 +1,45 @@ +use stm32f4xx_hal::{ + flash::{Error, FlashExt}, + stm32::FLASH, +}; +use sfkv::{Store, StoreBackend}; + +/// 16 KiB +pub const FLASH_SECTOR_SIZE: usize = 0x4000; +pub const FLASH_SECTOR: u8 = 12; +pub const FLASH_SECTOR_OFFSET: usize = 0x10_0000; +static mut BACKUP_SPACE: [u8; FLASH_SECTOR_SIZE] = [0; FLASH_SECTOR_SIZE]; + +pub struct FlashBackend { + flash: FLASH, +} + +impl StoreBackend for FlashBackend { + type Data = [u8]; + + fn data(&self) -> &Self::Data { + self.flash.read() + } + + type Error = Error; + fn erase(&mut self) -> Result<(), Self::Error> { + self.flash.unlocked().erase(FLASH_SECTOR) + } + + fn program(&mut self, offset: usize, payload: &[u8]) -> Result<(), Self::Error> { + self.flash.unlocked() + .program(FLASH_SECTOR_OFFSET + offset, payload.iter().cloned()) + } + + + fn backup_space(&self) -> &'static mut [u8] { + unsafe { &mut BACKUP_SPACE } + } +} + +pub type FlashStore = Store; + +pub fn store(flash: FLASH) -> FlashStore { + let backend = FlashBackend { flash }; + FlashStore::new(backend) +} diff --git a/src/main.rs b/src/main.rs index c590022..9e8c214 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,8 +64,8 @@ use channels::{CHANNELS, Channels}; mod channel; mod channel_state; mod config; -use config::Config; - +use config::ChannelConfig; +mod flash_store; const HSE: MegaHertz = MegaHertz(8); #[cfg(not(feature = "semihosting"))] @@ -73,8 +73,7 @@ const WATCHDOG_INTERVAL: u32 = 1_000; #[cfg(feature = "semihosting")] const WATCHDOG_INTERVAL: u32 = 30_000; -pub const EEPROM_PAGE_SIZE: usize = 8; -pub const EEPROM_SIZE: usize = 128; +const CHANNEL_CONFIG_KEY: [&str; 2] = ["ch0", "ch1"]; pub const DEFAULT_IPV4_ADDRESS: Ipv4Address = Ipv4Address([192, 168, 1, 26]); const TCP_PORT: u16 = 23; @@ -160,14 +159,22 @@ fn main() -> ! { usb::State::setup(usb); - let mut ipv4_address = DEFAULT_IPV4_ADDRESS; + let mut store = flash_store::store(dp.FLASH); + let mut store_value_buf = [0u8; 256]; + let mut channels = Channels::new(pins); - let _ = Config::load(&mut eeprom) - .map(|config| { - config.apply(&mut channels); - ipv4_address = Ipv4Address::from_bytes(&config.ipv4_address); - }) - .map_err(|e| warn!("error loading config: {:?}", e)); + for c in 0..CHANNELS { + match store.read_value::(CHANNEL_CONFIG_KEY[c]) { + Ok(Some(config)) => + config.apply(&mut channels, c), + Ok(None) => + error!("flash config not found for channel {}", c), + Err(e) => + error!("unable to load config {} from flash: {:?}", c, e), + } + } + + let mut ipv4_address = DEFAULT_IPV4_ADDRESS; info!("IPv4 address: {}", ipv4_address); // EEPROM ships with a read-only EUI-48 identifier @@ -341,22 +348,30 @@ fn main() -> ! { error!("unable to choose postfilter for rate {:.3}", rate), } } - Command::Load => { - match Config::load(&mut eeprom) { - Ok(config) => { - config.apply(&mut channels); - new_ipv4_address = Some(Ipv4Address::from_bytes(&config.ipv4_address)); + Command::Load { channel } => { + for c in 0..CHANNELS { + if channel.is_none() || channel == Some(c) { + match store.read_value::(CHANNEL_CONFIG_KEY[c]) { + Ok(Some(config)) => + config.apply(&mut channels, c), + Ok(None) => + error!("flash config not found"), + Err(e) => + error!("unable to load config from flash: {:?}", e), + } } - Err(e) => - error!("unable to load eeprom config: {:?}", e), } } - Command::Save => { - let config = Config::new(&mut channels, ipv4_address); - match config.save(&mut eeprom) { - Ok(()) => {}, - Err(e) => - error!("unable to save eeprom config: {:?}", e), + Command::Save { channel } => { + for c in 0..CHANNELS { + if channel.is_none() || channel == Some(c) { + let config = ChannelConfig::new(&mut channels, c); + let _ = store + .write_value(CHANNEL_CONFIG_KEY[c], &config, &mut store_value_buf) + .map_err( + |e| error!("unable to save config to flash: {:?}", e) + ); + } } } Command::Ipv4(address) => { diff --git a/src/steinhart_hart.rs b/src/steinhart_hart.rs index 842122e..87149df 100644 --- a/src/steinhart_hart.rs +++ b/src/steinhart_hart.rs @@ -8,12 +8,10 @@ use uom::si::{ ratio::ratio, thermodynamic_temperature::{degree_celsius, kelvin}, }; -use serde::Serialize; - -type JsonBuffer = heapless::Vec; +use serde::{Deserialize, Serialize}; /// Steinhart-Hart equation parameters -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Parameters { /// Base temperature pub t0: ThermodynamicTemperature, @@ -29,10 +27,6 @@ impl Parameters { let inv_temp = 1.0 / self.t0.get::() + (r / self.r0).get::().ln() / self.b; ThermodynamicTemperature::new::(1.0 / inv_temp) } - - pub fn to_json(&self) -> Result { - serde_json_core::to_vec(self) - } } impl Default for Parameters {