Compare commits

..

10 Commits

Author SHA1 Message Date
cac25d6006 linear dimming 2025-10-07 19:10:43 +01:00
97079c7b10 dimming 2025-10-07 18:38:53 +01:00
d66914f74f remove debugging 2025-10-04 15:45:54 +01:00
42c18a6d5a brightness scaling optimization 2025-10-04 15:01:50 +01:00
e98a24d32d timer debugging 2025-10-04 14:14:10 +01:00
556ac72d10 refactoring 2025-08-05 20:13:11 +01:00
f816a09554 control segment brightness; test pattern 2025-07-26 15:14:08 +01:00
6517cfd72b timer refactoring 2025-07-26 13:41:50 +01:00
9bec55bcc7 pointing to upstream crate URL 2025-07-18 21:41:30 +01:00
9b7df6b680 license update and README 2025-07-18 21:14:37 +01:00
7 changed files with 187 additions and 113 deletions

29
Cargo.lock generated
View File

@@ -27,9 +27,9 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "avr-calc"
@@ -37,7 +37,7 @@ version = "0.1.0"
dependencies = [
"arduino-hal",
"avr-device",
"calc-math",
"core-decimal-calc",
"embedded-hal 1.0.0",
"nb 1.1.0",
"ufmt",
@@ -89,19 +89,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603"
[[package]]
name = "calc-math"
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "core-decimal-calc"
version = "0.1.0"
source = "git+https://gitea.hexadust.net/hxd/core-decimal-calc.git#2a0b7a34f032ce58699c3944444ad19a11cfe2e8"
dependencies = [
"num-traits",
"ufmt",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "critical-section"
version = "1.2.0"
@@ -172,18 +173,18 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "proc-macro2"
version = "1.0.79"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]

View File

@@ -14,7 +14,7 @@ bench = false
ufmt = "0.2.0"
nb = "1.1.0"
embedded-hal = "1.0"
calc-math = { path = "../calc-math", features = ["ufmt"] }
core-decimal-calc = { git = "https://gitea.hexadust.net/hxd/core-decimal-calc.git", features = ["ufmt"] }
avr-device = "0.7.0"
[dependencies.arduino-hal]

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) [year] [fullname]
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.

View File

@@ -1,17 +1,14 @@
avr-calc
========
# Sinclair Scientific Calculator Emulator alternative software written in Rust
Alternative implementation for [Sinclair Scientific calculator](https://wiki.hexadust.net/books/electronics/page/sinclair-scientific-calculator-emulator-1974).
Alternative implementation for [Sinclair Scientific calculator](https://wiki.hexadust.net/books/electronics/page/sinclair-scientific-calculator-emulator-1974) board.
## Build Instructions
1. Install prerequisites as described in the [`avr-hal` README] (`avr-gcc`, `avr-libc`, `avrdude`, [`ravedude`]).
2. Run `cargo build` to build the firmware.
3. Run `cargo run` to flash the firmware to a connected board. If `ravedude` fails to detect your board, check its documentation at
2. Use `nightly-2024-03-22` toolchain.
2. Run `cargo build --release` to build the firmware (debug builds won't work for now).
3. Run `RAVEDUDE_PORT=/dev/ttyUSB0 cargo run --release` to flash the firmware to a connected board. If `ravedude` fails to detect your board, check its documentation at
<https://crates.io/crates/ravedude>.
4. `ravedude` will open a console session after flashing where you can interact with the UART console of your board.
[`avr-hal` README]: https://github.com/Rahix/avr-hal#readme
@@ -19,7 +16,7 @@ Alternative implementation for [Sinclair Scientific calculator](https://wiki.hex
## License
- ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
- [LICENSE-APACHE](LICENSE-APACHE)
## Floating point support

View File

@@ -5,7 +5,7 @@ use arduino_hal::{
port::{mode::Output, Pin},
};
use crate::DISPLAY_SEGMENTS;
use crate::{DISPLAY_SEGMENTS, IO_SEGMENT_ON_MAX_US, IO_SEGMENT_ON_MIN_US};
pub struct SegmentPins {
kd_seg_a: Pin<Output, PD5>,
@@ -82,11 +82,43 @@ impl SegmentPins {
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Segment(u8);
pub struct Brightness(u8);
impl Brightness {
pub const fn new(b: u8) -> Brightness {
Brightness(b)
}
pub const fn full() -> Brightness {
Brightness(u8::MAX)
}
pub const fn unwrap(self) -> u8 {
self.0
}
pub fn dimm(self, dimming: u8) -> Brightness {
Brightness(self.0.saturating_sub(dimming))
}
// Scales brightness (0-255) to range between IO_SEGMENT_ON_MIN_US and IO_SEGMENT_ON_MAX_US
pub fn scale_brightness(self) -> u32 {
// Using >> to avoid 32bit division which take ~576 cycles
IO_SEGMENT_ON_MIN_US + ((IO_SEGMENT_ON_MAX_US - IO_SEGMENT_ON_MIN_US) * self.0 as u32 >> 8)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Segment(u8, Brightness);
impl Segment {
pub fn new() -> Segment {
Segment(0)
Segment(0, Brightness::full())
}
pub fn brightness(&mut self, b: Brightness) -> &mut Self {
self.1 = b;
self
}
pub fn off(&mut self) -> &mut Self {
@@ -134,7 +166,7 @@ impl Segment {
self
}
pub fn apply(&self, seg: &mut SegmentPins) {
pub fn apply(&self, seg: &mut SegmentPins) -> Brightness {
seg.set_off();
if self.0 & 0b1000_0000 != 0 {
seg.set_a();
@@ -160,6 +192,7 @@ impl Segment {
if self.0 & 0b0000_0001 != 0 {
seg.set_dp();
}
self.1
}
pub fn num(&mut self, no: u8) -> &mut Self {

View File

@@ -12,23 +12,21 @@ use avr_device::{
asm::sleep,
interrupt::{CriticalSection, Mutex},
};
use calc_math::{
use core::cell::{Cell, RefCell};
use core_decimal_calc::{
calc::{StackCalc, StackCalcError},
Decimal,
};
use core::cell::RefCell;
use display::{DispalyState, SegmentPins, Show};
use keyboard::{Debounce, KeyPress, KeyReadout, Keyboard};
use arduino_hal::{
adc::channel::{ADC6, ADC7},
hal::port::PB5,
port::{mode::Output, Pin},
Adc,
};
use ufmt::derive::uDebug;
use crate::io::IOPins;
use crate::{display::Brightness, io::IOPins, timer::SegmentTimer};
// NOTE: 115200 @ 16MHz is 3.5% off, try 9600 or 1M if it causes issues (https://wormfood.net/avrbaudcalc.php)
const SERIAL_BAUD: u32 = 115200;
@@ -49,7 +47,14 @@ pub const DISPLAY_SEGMENT_EXP_MINUS: usize = 6;
// Timing
// Note: it takes ~224 μs to read keyboard after segment off
pub const IO_SEGMENT_RATE_US: u32 = 1000; // Time in μs between segment updates
pub const IO_SEGMENT_ON_US: u32 = 200; // How long in μs to hold segment LEDs on
pub const IO_SEGMENT_ON_MIN_US: u32 = 80; // How long in μs to hold segment LEDs on (dark)
pub const IO_SEGMENT_ON_MAX_US: u32 = 700; // How long in μs to hold segment LEDs on (bright)
// Dimming
pub const DISPLAY_FPS: u16 = (1_000_000 / (IO_SEGMENT_RATE_US * DISPLAY_SEGMENTS as u32)) as u16;
pub const DISPLAY_DIMM_FRAMES: u16 = DISPLAY_FPS * 10; // How many frames of inactivity before dimming
pub const DISPLAY_DIMM_SPEED: u8 = 4; // Dimm by amount every frame when sleeping
pub const DISPLAY_UNDIMM_SPEED: u8 = 16; // Brighten by amount every frame when not sleeping
// Calculator setup
pub const STACK_DEPTH: usize = 7;
@@ -62,47 +67,46 @@ type Calc = StackCalc<f32, STACK_DEPTH, 5, u8>;
// * Set another timer to run for 1ms.
// * On another timer interrupt expire disable display (LEDs off) and handle keyboard input.
static LED: Mutex<RefCell<Option<Pin<Output, PB5>>>> = Mutex::new(RefCell::new(None));
static IO_LOOP: Mutex<RefCell<Option<IOLoop>>> = Mutex::new(RefCell::new(None));
static KEY_PRESS: Mutex<RefCell<Option<KeyPress>>> = Mutex::new(RefCell::new(None));
static ADC: Mutex<RefCell<Option<Adc>>> = Mutex::new(RefCell::new(None));
// Values shared between main and interrupt handlers
type Global<T> = Mutex<Cell<T>>;
type RefGlobal<T> = Mutex<RefCell<Option<T>>>;
fn try_access<'cs, 'v: 'cs, T, O>(
static IO_LOOP: RefGlobal<IOLoop> = Mutex::new(RefCell::new(None));
static KEY_PRESS: Global<Option<KeyPress>> = Mutex::new(Cell::new(None));
static ADC: RefGlobal<Adc> = Mutex::new(RefCell::new(None));
static SEGMENT_TIMER: RefGlobal<SegmentTimer> = Mutex::new(RefCell::new(None));
fn access_global<'cs, 'v: 'cs, T, O>(
v: &'v Mutex<RefCell<Option<T>>>,
cs: CriticalSection<'cs>,
f: impl for<'t> FnOnce(&'t mut T) -> O,
) -> Option<O> {
if let Some(mut v) = v.borrow(cs).try_borrow_mut().ok() {
if let Some(v) = v.as_mut() {
return Some(f(v));
}
}
None
) -> O {
let mut v = v.borrow(cs).borrow_mut();
f(v.as_mut().unwrap())
}
#[avr_device::interrupt(atmega328p)]
unsafe fn TIMER0_COMPA() {
avr_device::interrupt::free(|cs| {
try_access(&LED, cs, |led| led.set_high()).expect("LED not available (COMPA)");
try_access(&IO_LOOP, cs, |io_loop| {
io_loop.display_on();
})
access_global(&IO_LOOP, cs, |io_loop| {
access_global(&SEGMENT_TIMER, cs, |st| {
let brightness = io_loop.display_on();
st.segment_on_time(brightness.scale_brightness());
});
});
});
}
#[avr_device::interrupt(atmega328p)]
unsafe fn TIMER0_COMPB() {
avr_device::interrupt::free(|cs| {
try_access(&LED, cs, |led| led.set_low()).expect("LED not available (COMPB)");
try_access(&IO_LOOP, cs, |io_loop| {
access_global(&IO_LOOP, cs, |io_loop| {
io_loop.display_off();
try_access(&ADC, cs, |adc| {
access_global(&ADC, cs, |adc| {
io_loop.read_key(adc);
});
if let Some(key) = io_loop.advance() {
if let Some(mut key_press) = KEY_PRESS.borrow(cs).try_borrow_mut().ok() {
key_press.replace(key);
}
KEY_PRESS.borrow(cs).replace(Some(key));
}
});
});
@@ -110,24 +114,30 @@ unsafe fn TIMER0_COMPB() {
pub struct IOLoop {
index: usize,
frame: u16,
io_pins: IOPins,
segment_pins: SegmentPins,
dispaly: DispalyState,
keyboard: Keyboard,
readount: Option<KeyReadout>,
debounce: Debounce,
sleep_timer: u16,
dimming: u8,
}
impl IOLoop {
pub fn new(io_pins: IOPins, segment_pins: SegmentPins, keyboard: Keyboard) -> IOLoop {
IOLoop {
index: 0,
frame: 0,
io_pins,
segment_pins,
dispaly: Default::default(),
keyboard,
readount: None,
debounce: Default::default(),
sleep_timer: 0,
dimming: u8::MAX,
}
}
@@ -146,21 +156,47 @@ impl IOLoop {
pub fn advance(&mut self) -> Option<KeyPress> {
self.index += 1;
if self.index == self.io_pins.len() || self.index == self.dispaly.len() {
// Frame done
self.frame = self.frame.wrapping_add(1);
// Start from first segment
self.index = 0;
// Full keyboard scan complete, debounce and return result
self.debounce.input(self.readount.take())
// Full keyboard scan complete, debounce
let key = self.debounce.input(self.readount.take());
// Reset or advance sleep timer
if key.is_some() {
self.sleep_timer = 0;
} else {
self.sleep_timer = self.sleep_timer.saturating_add(1);
}
if self.is_sleep() {
self.dimming = self.dimming.saturating_add(DISPLAY_DIMM_SPEED);
} else {
self.dimming = self.dimming.saturating_sub(DISPLAY_UNDIMM_SPEED);
}
key
} else {
None
}
}
pub fn display_on(&mut self) {
pub fn is_sleep(&self) -> bool {
self.sleep_timer >= DISPLAY_DIMM_FRAMES
}
pub fn dimming(&self) -> u8 {
self.dimming
}
pub fn display_on(&mut self) -> Brightness {
self.select_off();
let segment = self.dispaly[self.index];
segment.apply(&mut self.segment_pins);
let brighness = segment.apply(&mut self.segment_pins);
self.select_on();
brighness.dimm(self.dimming)
}
pub fn display_off(&mut self) {
@@ -175,6 +211,10 @@ impl IOLoop {
}
self.select_off();
}
pub fn frame(&self) -> (u16, usize) {
(self.frame, self.index)
}
}
#[derive(uDebug)]
@@ -512,8 +552,8 @@ fn main() -> ! {
);
let keyboard = Keyboard::new(ADC7, ADC6);
let io_loop = IOLoop::new(io_pins, segment_pins, keyboard);
let led = pins.d13.into_output();
let adc = Adc::new(dp.ADC, Default::default());
let segment_timer = SegmentTimer::init(dp.TC0, IO_SEGMENT_RATE_US);
let mut number_input = NumberInput::default();
@@ -527,15 +567,10 @@ fn main() -> ! {
};
avr_device::interrupt::free(|cs| {
LED.borrow(cs).replace(Some(led));
IO_LOOP.borrow(cs).replace(Some(io_loop));
ADC.borrow(cs).replace(Some(adc));
SEGMENT_TIMER.borrow(cs).replace(Some(segment_timer));
});
timer::segment_timer_init(
dp.TC0, // Timer0 (8bit)
IO_SEGMENT_RATE_US,
IO_SEGMENT_ON_US,
);
unsafe {
avr_device::interrupt::enable();
}
@@ -543,7 +578,7 @@ fn main() -> ! {
loop {
let mut key = None;
avr_device::interrupt::free(|cs| {
key = KEY_PRESS.borrow(cs).borrow_mut().take();
key = KEY_PRESS.borrow(cs).take();
});
if let TransientState::Done = state.transient {
@@ -602,15 +637,16 @@ fn main() -> ! {
.take(calc.len())
{
seg.dp();
seg.brightness(Brightness::full());
}
}
TransientState::Err { .. } => display.error(),
}
avr_device::interrupt::free(|cs| {
try_access(&IO_LOOP, cs, |io_loop| {
access_global(&IO_LOOP, cs, |io_loop| {
io_loop.update_display(&display);
});
})
});
}
}

View File

@@ -1,26 +1,54 @@
use arduino_hal::{clock::Clock, pac::TC0, DefaultClock};
// Sets up timer to rise two interrupts:
// 1. TIMER0_COMPA - every segment_rate_us μs
// 2. TIMER0_COMPB - segment_on_us μs after TIMER0_COMPA
pub fn segment_timer_init(tc0: TC0, segment_rate_us: u32, segment_on_us: u32) {
// Prescaler set for the timer (see tccr0b)
const TIMER_PRESCALE: u32 = 64;
// Timer clock tick rate per second
const TIMER_FREQ: u32 = DefaultClock::FREQ / TIMER_PRESCALE;
const fn us_to_ticks(us: u32) -> u32 {
TIMER_FREQ * us / 1_000_000
}
// Timer 0 (8bit)
pub struct SegmentTimer {
timer: TC0,
}
impl SegmentTimer {
// Sets up timer to rise interrupts:
// 1. TIMER0_COMPA - segment_switch_us - time in μs to switch to next segment
// 2. TIMER0_COMPB - set by set_segment_on_time to keep segment LEDs on
pub fn init(tc0: TC0, segment_switch_us: u32) -> SegmentTimer {
// 16_000_000 / 64 * 1000 / 1_000_000 => 250
let ocra = DefaultClock::FREQ / 64 * segment_rate_us / 1_000_000;
let ocrb = DefaultClock::FREQ / 64 * segment_on_us / 1_000_000;
assert!(ocra > ocrb);
let ocra = us_to_ticks(segment_switch_us);
// Use CTC mode: reset counter when matches compare value
tc0.tccr0a.write(|w| w.wgm0().ctc());
// Set the compare value for TOP (reset)
tc0.ocr0a
.write(|w| w.bits(ocra.try_into().expect("timer init seg_freq out of rage")));
// Set the compare value for B match
tc0.ocr0b
.write(|w| w.bits(ocrb.try_into().expect("timer init on_div out of rage")));
tc0.ocr0a.write(|w| {
w.bits(
ocra.try_into()
.expect("timer init segment_switch_us out of rage"),
)
});
// Slow down the timer (CLK / prescale)
tc0.tccr0b.write(|w| w.cs0().prescale_64());
// Raise interrupt on TOP (reset)
// Raise interrupt on B match
tc0.timsk0
.write(|w| w.ocie0a().set_bit().ocie0b().set_bit());
SegmentTimer { timer: tc0 }
}
// Set for how long the segment LEDs should be on in μs
// Controls TIMER0_COMPB interrupt time after TIMER0_COMPA
pub fn segment_on_time(&mut self, segment_on_us: u32) {
let delay: u8 = us_to_ticks(segment_on_us)
.try_into()
.expect("timer init segment_on_us out of rage");
let elapsed = self.timer.tcnt0.read().bits();
// Set the compare value for B match
self.timer.ocr0b.write(|w| w.bits(elapsed + delay));
}
}