1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
//! # Softpot resistive ribbon controller
//!
//! Spectra Symbol and others make resistive linear position sensors which may be used as ribbon controllers.
//!
//! Users play the ribbon by sliding their finger up and down a resistive track wired as a voltage divider.
//!
//! The position of the user's finger on the ribbon is represented as a number. The farther to the right the user is
//! pressing the larger the number. The position value is retained even when the user lifts their finger off of the
//! ribbon, similar to a sample-and-hold system. Some averaging is done to smooth out the raw readings and reduce the
//! influence of spurious inputs.
//!
//! Whether or not the user is pressing on the ribbon is represented as a boolean signal.
//!
//! The position value and finger-down signals are then typically used as control signals for other modules, such as
//! oscillators, filters, and amplifiers.
//!
//! # Inputs
//!
//! * Samples are fed into the ribbon controller
//!
//! # Outputs
//!
//! * The average of the most recent samples representing the position of the user's finger on the ribbon
//!
//! * Boolean signals related to the user's finger presses
//!
//! ---
//!
//! ## Note about the hardware
//!
//! The intended hardware setup can be [seen here](https://github.com/JordanAceto/synth-utils-rs/blob/main/images/ribbon_schematic_snippet.png)
//!
//! Referencing the schematic image linked above:
//! The ribbon is wired as a voltage divider between ground and a positive reference with a small series resistor `R1`
//! between the top of the ribbon and the positive ref. When the user is not pressing the ribbon the wiper is open
//! circuit. In order to detect finger presses pullup resistor `R2` is placed from the wiper to the positive reference.
//! With this setup, we can tell if the ribbon is not being pressed because `R2` pulls all the way up to the maximum
//! value. However, when a person is pressing the ribbon the maximum value is limited by the small series resistor `R1`
//! and is always lower than the "no finger pullup" value.
//!
//! Resistor `R1` is chosen to be small enough that not too much range is wasted but large enough that we can reliably
//! detect no-press conditions even with the presence of some noise. Resistor `R2` is chosen to be large enough that it
//! doesn't bend the response of the voltage divider too much but small enough that the voltage shoots up to full scale
//! quickly when the user lifts their finger. The opamp buffer is optional, but recommended to provide a low impedance
//! source to feed the ADC and provide some noise immunity if the ribbon is physically far from the pcb and connected
//! by long wires.
//!
//! It is expected that software external to this module will read the ADC and convert the raw integer-based ADC value
//! into a floating point number in the range `[0.0, 1.0]` before interfacing with this ribbon module.
//!
//! Note that this software module has no direct connection to the physical hardware. It is assumed that samples come
//! from to feed the ribbon the specified hardware setup which is sampled by an Analog to Digital Converter, but we
//! could just as easily feed it made up samples from anywhere. This allows us to have some flexibility in using this
//! module with various microcontroller setups. The above schematic snippet and description are included to illustrate
//! one way that this module could be used.
//!
//! ---
use heapless::HistoryBuffer;
/// A synthesizer ribbon controller is represented here.
///
/// It is expected to use the provided `sample_rate_to_capacity(sr)` const function to calculate the const generic
/// `BUFFER_CAPACITY` argument. If in the future Rust offers a way to calculate the buffer capacity in a more
/// straightforward way this should be changed.
pub struct RibbonController<const BUFFER_CAPACITY: usize> {
/// Samples below this value indicate that there is a finger pressed down on the ribbon.
///
/// The value must be in [0.0, +1.0], and represents the fraction of the ADC reading which counts as a finger press.
///
/// The exact value depends on the resistor chosen that connects the top of the ribbon to the positive voltage
/// reference. We "waste" a little bit of the voltage range of the ribbon as a dead-zone so we can clearly detect when
/// the user is pressing the ribbon or not.
finger_press_high_boundary: f32,
/// error scaling constant used to un-bend the ribbon which is non-linear due to the pullup resistor
error_const: f32,
/// The current position value of the ribbon
current_val: f32,
/// The current gate value of the ribbon
finger_is_pressing: bool,
/// True iff the gate is rising after being low
finger_just_pressed: bool,
/// True iff the gate is falling after being high
finger_just_released: bool,
/// An internal buffer for storing and averaging samples as they come in via the `poll` method
buff: HistoryBuffer<f32, BUFFER_CAPACITY>,
/// The number of samples to ignore when the user initially presses their finger
num_to_ignore_up_front: usize,
/// The number of the most recent sampes to discard
num_to_discard_at_end: usize,
/// The number of samples revieved since the user pressed their finger down
///
/// Resets when the user lifts their finger
num_samples_received: usize,
/// The number of samples actually written to the buffer
///
/// Resets when the user lifts their finger
num_samples_written: usize,
}
impl<const BUFFER_CAPACITY: usize> RibbonController<BUFFER_CAPACITY> {
/// `Ribbon::new(sr, sp, dr, pu)` is a new Ribbon controller
///
/// # Arguments:
///
/// * `sample_rate_hz` - The sample rate in Hertz
///
/// * `softpot_ohms` - The end-to-end resistance of the softpot used, typically 10k or 20k
///
/// * `dropper_resistor_ohms` - The value of the resistor which sits between the top of the softpot and the positive
/// voltage reference.
///
/// * `pullup_resistor_ohms` - The value of the wiper pullup reistor, shoudl be at least 10x softpot_ohms or larger
pub fn new(
sample_rate_hz: f32,
softpot_ohms: f32,
dropper_resistor_ohms: f32,
pullup_resistor_ohms: f32,
) -> Self {
Self {
finger_press_high_boundary: 1.0
- (dropper_resistor_ohms / (dropper_resistor_ohms + softpot_ohms)),
error_const: (softpot_ohms + dropper_resistor_ohms) / pullup_resistor_ohms,
current_val: 0.0_f32,
finger_is_pressing: false,
finger_just_pressed: false,
finger_just_released: false,
buff: HistoryBuffer::new(),
num_to_ignore_up_front: ((sample_rate_hz as u32 * RIBBON_FALL_TIME_USEC) / 1_000_000)
as usize,
num_to_discard_at_end: ((sample_rate_hz as u32 * RIBBON_RISE_TIME_USEC) / 1_000_000)
as usize,
num_samples_received: 0,
num_samples_written: 0,
}
}
/// `rib.poll(raw_adc_value)` updates the controller by polling the raw ADC signal. Must be called at the sample rate
///
/// # Arguments
///
/// * `raw_adc_value` - the raw ADC signal to poll in `[0.0, 1.0]`, represents the finger position on the ribbon.
/// Inputs outside of the range `[0.0, 1.0]` are undefined.
/// Note that a small portion of the range at the top near +1.0 is expected to be "eaten" by the series resistor
pub fn poll(&mut self, raw_adc_value: f32) {
let user_is_pressing_ribbon = raw_adc_value < self.finger_press_high_boundary;
if user_is_pressing_ribbon {
self.num_samples_received += 1;
self.num_samples_received = self.num_samples_received.min(self.num_to_ignore_up_front);
// only start adding samples to the buffer after we've ignored a few potentially spurious initial samples
if self.num_to_ignore_up_front <= self.num_samples_received {
self.buff.write(raw_adc_value);
self.num_samples_written += 1;
self.num_samples_written = self.num_samples_written.min(self.buff.capacity());
// is the buffer full?
if self.num_samples_written == self.buff.capacity() {
let num_to_take = self.buff.capacity() - self.num_to_discard_at_end;
// take the average of the most recent samples, minus a few of the very most recent ones which might be
// shooting up towards full scale when the user lifts their finger
self.current_val = self.buff.oldest_ordered().take(num_to_take).sum::<f32>()
/ (num_to_take as f32);
self.current_val -= self.error_estimate(self.current_val);
// if this flag is false right now then they must have just pressed their finger down
if !self.finger_is_pressing {
self.finger_just_pressed = true;
self.finger_is_pressing = true;
}
}
}
} else {
// if this flag is true right now then they must have just lifted their finger
if self.finger_is_pressing {
self.finger_just_released = true;
self.num_samples_received = 0;
self.num_samples_written = 0;
self.finger_is_pressing = false;
}
}
}
/// `rib.value()` is the current position value of the ribbon in `[0.0, 1.0]`
///
/// If the user's finger is not pressing on the ribbon, the last valid value before they lifted their finger
/// is returned.
///
/// The value is expanded to take up the whole `[0.0, 1.0]`range, so even though the input will not quite reach
/// +1.0 at the top end (due to the series resistance) the output will reach or at least come very close to +1.0
pub fn value(&self) -> f32 {
// scale the value back to full scale since we loose a tiny bit of range to the high-boundary
self.current_val / self.finger_press_high_boundary
}
/// `rib.finger_is_pressing()` is `true` iff the user is pressing on the ribbon.
pub fn finger_is_pressing(&self) -> bool {
self.finger_is_pressing
}
/// `rib.finger_just_pressed()` is `true` iff the user has just pressed the ribbon after having not touched it.
///
/// Self clearing
pub fn finger_just_pressed(&mut self) -> bool {
if self.finger_just_pressed {
self.finger_just_pressed = false;
true
} else {
false
}
}
/// `rib.finger_just_released()` is `true` iff the user has just lifted their finger off the ribbon.
///
/// Self clearing
pub fn finger_just_released(&mut self) -> bool {
if self.finger_just_released {
self.finger_just_released = false;
true
} else {
false
}
}
/// `rib.error_estimate(p)` is the estimated error at position `p` resulting from the influence of the pullup resistor
///
/// The softpot is wired as a voltage divider with an additional pullup resistor from the wiper to the positive ref.
/// The pullup resistor bends the Vout so that it is not linear, Vout rises faster than it would without the pullup.
///
/// This error estimation approximate, but can help straighten out the ribbon response
///
/// # Arguments:
///
/// * `pos` - the position value in `[0.0, 1.0]`
fn error_estimate(&self, pos: f32) -> f32 {
(pos - pos * pos) * self.error_const
}
}
/// The approximate measured time it takes for the ribbon to settle on a low value after the user presses their finger.
///
/// We want to ignore samples taken while the ribbon is settling during a finger-press value.
///
/// Rounded up a bit from the actual measured value, better to take a little extra time than to include bad input.
const RIBBON_FALL_TIME_USEC: u32 = 1_000;
/// The approximate measured time it takes the ribbon to rise to the pull-up value after releasing your finger.
///
/// We want to ignore samples that are taken while the ribbon is shooting up towards full scale after lifting a finger.
///
/// Rounded up a bit from the actual measured value, better to take a little extra time than to include bad input.
const RIBBON_RISE_TIME_USEC: u32 = 2_000;
/// The minimum time required to capture a reading
///
/// Ideally several times longer than the sum of the RISE and FALL times
const MIN_CAPTURE_TIME_USEC: u32 = (RIBBON_FALL_TIME_USEC + RIBBON_RISE_TIME_USEC) * 5;
/// `sample_rate_to_capacity(sr_hz)` is the calculated capacity needed for the internal buffer based on the sample rate.
///
/// Const function allows us to use the result of this expression as a generic argument when we create ribbon objects.
/// If rust support for generic expressions improves, this function could be refactored out.
///
/// The capacity needs space for the main samples that we will actually care about, as well as room for the most
/// recent samples to discard. This is to avoid including spurious readings in the average.
pub const fn sample_rate_to_capacity(sample_rate_hz: u32) -> usize {
// can't use floats in const function yet
let num_main_samples_to_care_about =
((sample_rate_hz * MIN_CAPTURE_TIME_USEC) / 1_000_000) as usize;
let num_to_discard_at_end = ((sample_rate_hz * RIBBON_RISE_TIME_USEC) / 1_000_000) as usize;
num_main_samples_to_care_about + num_to_discard_at_end + 1
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_RATE: f32 = 10_000.0;
const RIBBON_BUFF_CAPACITY: usize = sample_rate_to_capacity(SAMPLE_RATE as u32);
/// `test_ribbon()` is a basic ribbon controller for testing
fn test_ribbon() -> RibbonController<RIBBON_BUFF_CAPACITY> {
RibbonController::new(SAMPLE_RATE as f32, 20E3, 820.0, 1E6)
}
// a bit glass-boxy, but hard to test otherwise, hand calculated by inspecting the code
const _TEST_RIB_NUM_TO_IGNORE_UP_FRONT: u32 = 10;
const TEST_RIB_NUM_TO_IGNORE_AT_END: u32 = 20;
const _TEST_RIB_NUM_TO_CARE_ABOUT: u32 = 150;
const _TEST_RIB_CAPACITY: u32 = 170;
const TEST_RIB_NUM_FOR_VALID_READING: u32 = 180;
#[test]
fn should_have_dead_zone_before_value_is_captured() {
let mut rib = test_ribbon();
// poll some samples, but not enough to get a reading yet
for _ in 0..(TEST_RIB_NUM_FOR_VALID_READING - 1) {
rib.poll(0.42);
}
assert!(!rib.finger_is_pressing());
}
#[test]
fn should_eventually_register_reading_with_enough_polling() {
let mut rib = test_ribbon();
// poll some samples, but not enough to get a reading yet
for _ in 0..TEST_RIB_NUM_FOR_VALID_READING - 1 {
rib.poll(0.42);
}
assert!(!rib.finger_is_pressing());
rib.poll(0.42);
assert!(rib.finger_is_pressing());
}
#[test]
fn one_oob_poll_means_finger_not_pressing() {
let mut rib = test_ribbon();
// poll enough to register a reading
for _ in 0..TEST_RIB_NUM_FOR_VALID_READING {
rib.poll(0.42);
}
assert!(rib.finger_is_pressing());
// 1.0 is always out-of-bounds, there is always some lost to the resistor
rib.poll(1.0);
assert!(!rib.finger_is_pressing());
}
#[test]
fn last_val_retained_after_finger_lifted() {
let mut rib = test_ribbon();
// poll enough to register a reading
for _ in 0..TEST_RIB_NUM_FOR_VALID_READING {
rib.poll(0.42);
}
let old_val = rib.value();
// 1.0 is always out-of-bounds, there is always some lost to the resistor
rib.poll(1.0);
assert!(!rib.finger_is_pressing());
assert_eq!(rib.value(), old_val);
}
#[test]
fn bigger_inputs_increase_output() {
let mut rib = test_ribbon();
// poll enough to register a reading
for _ in 0..TEST_RIB_NUM_FOR_VALID_READING {
rib.poll(0.1);
}
let old_val = rib.value();
// do some polling with the new val but don't fill the buffer entirely with new stuff
for _ in 0..TEST_RIB_NUM_FOR_VALID_READING / 2 {
rib.poll(0.2);
}
assert!(old_val < rib.value());
}
#[test]
fn smaller_inputs_decrease_output() {
let mut rib = test_ribbon();
// poll enough to register a reading
for _ in 0..TEST_RIB_NUM_FOR_VALID_READING {
rib.poll(0.7);
}
let old_val = rib.value();
for _ in 0..TEST_RIB_NUM_FOR_VALID_READING / 4 {
rib.poll(0.6);
}
assert!(rib.value() < old_val);
}
#[test]
fn rising_gate_triggers() {
let mut rib = test_ribbon();
for _ in 0..TEST_RIB_NUM_FOR_VALID_READING {
rib.poll(0.1);
}
assert!(rib.finger_just_pressed());
// it's self clearing
assert!(!rib.finger_just_pressed());
// still pressing the ribbon, so no new just-pressed
rib.poll(0.1);
assert!(!rib.finger_just_pressed());
}
#[test]
fn last_few_inputs_are_ignored() {
let mut rib = test_ribbon();
// poll enough to register a reading
for _ in 0..TEST_RIB_NUM_FOR_VALID_READING {
rib.poll(0.0);
}
assert!(rib.finger_is_pressing());
assert_eq!(rib.value(), 0.0);
//add a few valid readings at the end, which will be ignored
for _ in 0..TEST_RIB_NUM_TO_IGNORE_AT_END {
rib.poll(0.9);
}
assert_eq!(rib.value(), 0.0);
// one more sample will be factored in to the average
rib.poll(0.9);
assert!(0.0 < rib.value());
}
}