rewrite config for sfkv-based flash_store

This commit is contained in:
Astro 2020-12-12 01:25:07 +01:00
parent 088bd6eb76
commit 383ebcd8e4
8 changed files with 197 additions and 252 deletions

13
Cargo.lock generated
View File

@ -391,6 +391,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "sfkv"
version = "0.1.0"
dependencies = [
"byteorder",
"postcard",
"serde",
]
[[package]] [[package]]
name = "smoltcp" name = "smoltcp"
version = "0.6.0" version = "0.6.0"
@ -436,7 +445,6 @@ dependencies = [
[[package]] [[package]]
name = "stm32f4xx-hal" name = "stm32f4xx-hal"
version = "0.8.3" version = "0.8.3"
source = "git+https://github.com/stm32-rs/stm32f4xx-hal.git#e80925770d2fe72f0f01a7b46147f4e31d512689"
dependencies = [ dependencies = [
"bare-metal 0.2.5", "bare-metal 0.2.5",
"cast", "cast",
@ -444,6 +452,7 @@ dependencies = [
"cortex-m-rt", "cortex-m-rt",
"embedded-dma", "embedded-dma",
"embedded-hal", "embedded-hal",
"log",
"nb 0.1.3", "nb 0.1.3",
"rand_core", "rand_core",
"stm32f4", "stm32f4",
@ -491,9 +500,9 @@ dependencies = [
"num-traits", "num-traits",
"panic-abort", "panic-abort",
"panic-semihosting", "panic-semihosting",
"postcard",
"serde", "serde",
"serde-json-core", "serde-json-core",
"sfkv",
"smoltcp", "smoltcp",
"stm32-eth", "stm32-eth",
"stm32f4xx-hal", "stm32f4xx-hal",

View File

@ -34,9 +34,9 @@ nb = "1"
uom = { version = "0.30", default-features = false, features = ["autoconvert", "si", "f64", "use_serde"] } uom = { version = "0.30", default-features = false, features = ["autoconvert", "si", "f64", "use_serde"] }
eeprom24x = "0.3" eeprom24x = "0.3"
serde = { version = "1.0", default-features = false, features = ["derive"] } serde = { version = "1.0", default-features = false, features = ["derive"] }
postcard = "0.5"
heapless = "0.5" heapless = "0.5"
serde-json-core = "0.1" serde-json-core = "0.1"
sfkv = "0.1"
[patch.crates-io] [patch.crates-io]
stm32f4xx-hal = { git = "https://github.com/stm32-rs/stm32f4xx-hal.git" } stm32f4xx-hal = { git = "https://github.com/stm32-rs/stm32f4xx-hal.git" }

View File

@ -118,10 +118,10 @@ The scope of this setting is per TCP session.
| `postfilter` | Show postfilter settings | | `postfilter` | Show postfilter settings |
| `postfilter <0/1> off` | Disable postfilter | | `postfilter <0/1> off` | Disable postfilter |
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate | | `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
| `load` | Restore configuration from EEPROM | | `load [0/1]` | Restore configuration for channel all/0/1 from flash |
| `save` | Save configuration to EEPROM | | `save [0/1]` | Save configuration for channel all/0/1 to flash |
| `reset` | Reset the device | | `reset` | Reset the device |
| `ipv4 <X.X.X.X>` | Configure IPv4 address | | `ipv4 <X.X.X.X/L> [Y.Y.Y.Y]` | Configure IPv4 address, netmask length, and optional default gateway |
## USB ## USB

View File

@ -132,8 +132,12 @@ pub enum CenterPoint {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Command { pub enum Command {
Quit, Quit,
Load, Load {
Save, channel: Option<usize>,
},
Save {
channel: Option<usize>,
},
Reset, Reset,
Ipv4([u8; 4]), Ipv4([u8; 4]),
Show(ShowCommand), Show(ShowCommand),
@ -437,6 +441,38 @@ fn postfilter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
))(input) ))(input)
} }
fn load(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
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<Command, Error>> {
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<Command, Error>> { fn ipv4(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("ipv4")(input)?; let (input, _) = tag("ipv4")(input)?;
let (input, _) = whitespace(input)?; let (input, _) = whitespace(input)?;
@ -457,8 +493,8 @@ fn ipv4(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> { fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
alt((value(Ok(Command::Quit), tag("quit")), alt((value(Ok(Command::Quit), tag("quit")),
value(Ok(Command::Load), tag("load")), load,
value(Ok(Command::Save), tag("save")), save,
value(Ok(Command::Reset), tag("reset")), value(Ok(Command::Reset), tag("reset")),
ipv4, ipv4,
map(report, Ok), map(report, Ok),
@ -496,13 +532,25 @@ mod test {
#[test] #[test]
fn parse_load() { fn parse_load() {
let command = Command::parse(b"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] #[test]
fn parse_save() { fn parse_save() {
let command = Command::parse(b"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] #[test]

View File

@ -1,102 +1,24 @@
use postcard::{from_bytes, to_slice};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use smoltcp::wire::Ipv4Address; use smoltcp::wire::Ipv4Address;
use stm32f4xx_hal::i2c;
use uom::si::{ use uom::si::{
electric_potential::volt, electric_potential::volt,
electric_current::ampere, electric_current::ampere,
electrical_resistance::ohm, f64::{ElectricCurrent, ElectricPotential},
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature},
thermodynamic_temperature::degree_celsius,
}; };
use crate::{ use crate::{
ad7172::PostFilter, ad7172::PostFilter,
channels::{CHANNELS, Channels}, channels::Channels,
command_parser::CenterPoint, command_parser::CenterPoint,
EEPROM_SIZE, EEPROM_PAGE_SIZE,
pid, pid,
pins,
steinhart_hart, steinhart_hart,
}; };
#[derive(Debug)]
pub enum Error {
Eeprom(eeprom24x::Error<i2c::Error>),
Encode(postcard::Error),
}
impl From<eeprom24x::Error<i2c::Error>> for Error {
fn from(e: eeprom24x::Error<i2c::Error>) -> Self {
Error::Eeprom(e)
}
}
impl From<postcard::Error> 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<Self, Error> {
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)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ChannelConfig { pub struct ChannelConfig {
center: CenterPoint, center: CenterPoint,
pid: pid::Parameters, pid: pid::Parameters,
pid_target: f32, pid_target: f32,
sh: SteinhartHartConfig, sh: steinhart_hart::Parameters,
pwm: PwmLimits, pwm: PwmLimits,
/// uses variant `PostFilter::Invalid` instead of `None` to save space /// uses variant `PostFilter::Invalid` instead of `None` to save space
adc_postfilter: PostFilter, adc_postfilter: PostFilter,
@ -115,7 +37,7 @@ impl ChannelConfig {
center: state.center.clone(), center: state.center.clone(),
pid: state.pid.parameters.clone(), pid: state.pid.parameters.clone(),
pid_target: state.pid.target as f32, pid_target: state.pid.target as f32,
sh: (&state.sh).into(), sh: state.sh.clone(),
pwm, pwm,
adc_postfilter, adc_postfilter,
} }
@ -126,7 +48,7 @@ impl ChannelConfig {
state.center = self.center.clone(); state.center = self.center.clone();
state.pid.parameters = self.pid.clone(); state.pid.parameters = self.pid.clone();
state.pid.target = self.pid_target.into(); state.pid.target = self.pid_target.into();
state.sh = (&self.sh).into(); state.sh = self.sh.clone();
self.pwm.apply(channels, channel); 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::<degree_celsius>() as f32,
r0: sh.r0.get::<ohm>() as f32,
b: sh.b as f32,
}
}
}
impl Into<steinhart_hart::Parameters> for &SteinhartHartConfig {
fn into(self) -> steinhart_hart::Parameters {
steinhart_hart::Parameters {
t0: ThermodynamicTemperature::new::<degree_celsius>(self.t0.into()),
r0: ElectricalResistance::new::<ohm>(self.r0.into()),
b: self.b.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct PwmLimits { struct PwmLimits {
max_v: f32, max_v: f64,
max_i_pos: f32, max_i_pos: f64,
max_i_neg: f32, max_i_neg: f64,
} }
impl PwmLimits { impl PwmLimits {
@ -178,76 +73,15 @@ impl PwmLimits {
let (max_i_pos, _) = channels.get_max_i_pos(channel); let (max_i_pos, _) = channels.get_max_i_pos(channel);
let (max_i_neg, _) = channels.get_max_i_neg(channel); let (max_i_neg, _) = channels.get_max_i_neg(channel);
PwmLimits { PwmLimits {
max_v: max_v.get::<volt>() as f32, max_v: max_v.get::<volt>(),
max_i_pos: max_i_pos.get::<ampere>() as f32, max_i_pos: max_i_pos.get::<ampere>(),
max_i_neg: max_i_neg.get::<ampere>() as f32, max_i_neg: max_i_neg.get::<ampere>(),
} }
} }
pub fn apply(&self, channels: &mut Channels, channel: usize) { pub fn apply(&self, channels: &mut Channels, channel: usize) {
channels.set_max_v(channel, ElectricPotential::new::<volt>(self.max_v.into())); channels.set_max_v(channel, ElectricPotential::new::<volt>(self.max_v));
channels.set_max_i_pos(channel, ElectricCurrent::new::<ampere>(self.max_i_pos.into())); channels.set_max_i_pos(channel, ElectricCurrent::new::<ampere>(self.max_i_pos));
channels.set_max_i_neg(channel, ElectricCurrent::new::<ampere>(self.max_i_neg.into())); channels.set_max_i_neg(channel, ElectricCurrent::new::<ampere>(self.max_i_neg));
}
}
#[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);
} }
} }

45
src/flash_store.rs Normal file
View File

@ -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<FlashBackend>;
pub fn store(flash: FLASH) -> FlashStore {
let backend = FlashBackend { flash };
FlashStore::new(backend)
}

View File

@ -64,8 +64,8 @@ use channels::{CHANNELS, Channels};
mod channel; mod channel;
mod channel_state; mod channel_state;
mod config; mod config;
use config::Config; use config::ChannelConfig;
mod flash_store;
const HSE: MegaHertz = MegaHertz(8); const HSE: MegaHertz = MegaHertz(8);
#[cfg(not(feature = "semihosting"))] #[cfg(not(feature = "semihosting"))]
@ -73,8 +73,7 @@ const WATCHDOG_INTERVAL: u32 = 1_000;
#[cfg(feature = "semihosting")] #[cfg(feature = "semihosting")]
const WATCHDOG_INTERVAL: u32 = 30_000; const WATCHDOG_INTERVAL: u32 = 30_000;
pub const EEPROM_PAGE_SIZE: usize = 8; const CHANNEL_CONFIG_KEY: [&str; 2] = ["ch0", "ch1"];
pub const EEPROM_SIZE: usize = 128;
pub const DEFAULT_IPV4_ADDRESS: Ipv4Address = Ipv4Address([192, 168, 1, 26]); pub const DEFAULT_IPV4_ADDRESS: Ipv4Address = Ipv4Address([192, 168, 1, 26]);
const TCP_PORT: u16 = 23; const TCP_PORT: u16 = 23;
@ -160,14 +159,22 @@ fn main() -> ! {
usb::State::setup(usb); 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 mut channels = Channels::new(pins);
let _ = Config::load(&mut eeprom) for c in 0..CHANNELS {
.map(|config| { match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
config.apply(&mut channels); Ok(Some(config)) =>
ipv4_address = Ipv4Address::from_bytes(&config.ipv4_address); config.apply(&mut channels, c),
}) Ok(None) =>
.map_err(|e| warn!("error loading config: {:?}", e)); 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); info!("IPv4 address: {}", ipv4_address);
// EEPROM ships with a read-only EUI-48 identifier // EEPROM ships with a read-only EUI-48 identifier
@ -341,22 +348,30 @@ fn main() -> ! {
error!("unable to choose postfilter for rate {:.3}", rate), error!("unable to choose postfilter for rate {:.3}", rate),
} }
} }
Command::Load => { Command::Load { channel } => {
match Config::load(&mut eeprom) { for c in 0..CHANNELS {
Ok(config) => { if channel.is_none() || channel == Some(c) {
config.apply(&mut channels); match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
new_ipv4_address = Some(Ipv4Address::from_bytes(&config.ipv4_address)); Ok(Some(config)) =>
} config.apply(&mut channels, c),
Ok(None) =>
error!("flash config not found"),
Err(e) => Err(e) =>
error!("unable to load eeprom config: {:?}", e), error!("unable to load config from flash: {:?}", e),
} }
} }
Command::Save => { }
let config = Config::new(&mut channels, ipv4_address); }
match config.save(&mut eeprom) { Command::Save { channel } => {
Ok(()) => {}, for c in 0..CHANNELS {
Err(e) => if channel.is_none() || channel == Some(c) {
error!("unable to save eeprom config: {:?}", e), 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) => { Command::Ipv4(address) => {

View File

@ -8,12 +8,10 @@ use uom::si::{
ratio::ratio, ratio::ratio,
thermodynamic_temperature::{degree_celsius, kelvin}, thermodynamic_temperature::{degree_celsius, kelvin},
}; };
use serde::Serialize; use serde::{Deserialize, Serialize};
type JsonBuffer = heapless::Vec<u8, heapless::consts::U200>;
/// Steinhart-Hart equation parameters /// Steinhart-Hart equation parameters
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Parameters { pub struct Parameters {
/// Base temperature /// Base temperature
pub t0: ThermodynamicTemperature, pub t0: ThermodynamicTemperature,
@ -29,10 +27,6 @@ impl Parameters {
let inv_temp = 1.0 / self.t0.get::<kelvin>() + (r / self.r0).get::<ratio>().ln() / self.b; let inv_temp = 1.0 / self.t0.get::<kelvin>() + (r / self.r0).get::<ratio>().ln() / self.b;
ThermodynamicTemperature::new::<kelvin>(1.0 / inv_temp) ThermodynamicTemperature::new::<kelvin>(1.0 / inv_temp)
} }
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
} }
impl Default for Parameters { impl Default for Parameters {