diff options
| author | xoviat <[email protected]> | 2025-11-22 14:38:49 +0000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-11-22 14:38:49 +0000 |
| commit | 7eff7477746a38364c5c62020b23f578b69fdcfd (patch) | |
| tree | 2e7557bc3aa21d912e807f4ecfba4a49ef67bcbb | |
| parent | bbbeb6cea5551afdc21274101ffb4194c81b0987 (diff) | |
| parent | f686ce9ebb19081e12eae203e92273f8ecb6eaf2 (diff) | |
Merge pull request #4925 from diondokter/u0-lcd
[STM32] LCD
| -rw-r--r-- | embassy-stm32/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | embassy-stm32/Cargo.toml | 4 | ||||
| -rw-r--r-- | embassy-stm32/build.rs | 18 | ||||
| -rw-r--r-- | embassy-stm32/src/lcd.rs | 510 | ||||
| -rw-r--r-- | embassy-stm32/src/lib.rs | 2 | ||||
| -rw-r--r-- | examples/stm32u0/.cargo/config.toml | 4 | ||||
| -rw-r--r-- | examples/stm32u0/Cargo.toml | 6 | ||||
| -rw-r--r-- | examples/stm32u0/src/bin/lcd.rs | 412 |
8 files changed, 948 insertions, 9 deletions
diff --git a/embassy-stm32/CHANGELOG.md b/embassy-stm32/CHANGELOG.md index 87a8ef7c9..ad6a512b8 100644 --- a/embassy-stm32/CHANGELOG.md +++ b/embassy-stm32/CHANGELOG.md | |||
| @@ -74,6 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| 74 | - fix: fixing channel numbers on vbat and vddcore for adc on adc | 74 | - fix: fixing channel numbers on vbat and vddcore for adc on adc |
| 75 | - adc: adding disable to vbat | 75 | - adc: adding disable to vbat |
| 76 | - feat: stm32/flash: add async support for h7 family | 76 | - feat: stm32/flash: add async support for h7 family |
| 77 | - feat: stm32/lcd: added implementation | ||
| 77 | 78 | ||
| 78 | ## 0.4.0 - 2025-08-26 | 79 | ## 0.4.0 - 2025-08-26 |
| 79 | 80 | ||
diff --git a/embassy-stm32/Cargo.toml b/embassy-stm32/Cargo.toml index 2f4f2ce51..fbd00895d 100644 --- a/embassy-stm32/Cargo.toml +++ b/embassy-stm32/Cargo.toml | |||
| @@ -200,11 +200,11 @@ aligned = "0.4.1" | |||
| 200 | heapless = "0.9.1" | 200 | heapless = "0.9.1" |
| 201 | 201 | ||
| 202 | #stm32-metapac = { version = "18" } | 202 | #stm32-metapac = { version = "18" } |
| 203 | stm32-metapac = { git = "https://github.com/embassy-rs/stm32-data-generated", tag = "stm32-data-b77c8d968f53b18d6bdcd052e354b5070ec2bbc2" } | 203 | stm32-metapac = { git = "https://github.com/embassy-rs/stm32-data-generated", tag = "stm32-data-61abfb115273dc66ed99bf14545779a2aa0722c8" } |
| 204 | 204 | ||
| 205 | [build-dependencies] | 205 | [build-dependencies] |
| 206 | #stm32-metapac = { version = "18", default-features = false, features = ["metadata"]} | 206 | #stm32-metapac = { version = "18", default-features = false, features = ["metadata"]} |
| 207 | stm32-metapac = { git = "https://github.com/embassy-rs/stm32-data-generated", tag = "stm32-data-b77c8d968f53b18d6bdcd052e354b5070ec2bbc2", default-features = false, features = ["metadata"] } | 207 | stm32-metapac = { git = "https://github.com/embassy-rs/stm32-data-generated", tag = "stm32-data-61abfb115273dc66ed99bf14545779a2aa0722c8", default-features = false, features = ["metadata"] } |
| 208 | 208 | ||
| 209 | proc-macro2 = "1.0.36" | 209 | proc-macro2 = "1.0.36" |
| 210 | quote = "1.0.15" | 210 | quote = "1.0.15" |
diff --git a/embassy-stm32/build.rs b/embassy-stm32/build.rs index 8cbd38e10..fa493da84 100644 --- a/embassy-stm32/build.rs +++ b/embassy-stm32/build.rs | |||
| @@ -1352,6 +1352,9 @@ fn main() { | |||
| 1352 | (("tsc", "G8_IO2"), quote!(crate::tsc::G8IO2Pin)), | 1352 | (("tsc", "G8_IO2"), quote!(crate::tsc::G8IO2Pin)), |
| 1353 | (("tsc", "G8_IO3"), quote!(crate::tsc::G8IO3Pin)), | 1353 | (("tsc", "G8_IO3"), quote!(crate::tsc::G8IO3Pin)), |
| 1354 | (("tsc", "G8_IO4"), quote!(crate::tsc::G8IO4Pin)), | 1354 | (("tsc", "G8_IO4"), quote!(crate::tsc::G8IO4Pin)), |
| 1355 | (("lcd", "SEG"), quote!(crate::lcd::SegPin)), | ||
| 1356 | (("lcd", "COM"), quote!(crate::lcd::ComPin)), | ||
| 1357 | (("lcd", "VLCD"), quote!(crate::lcd::VlcdPin)), | ||
| 1355 | (("dac", "OUT1"), quote!(crate::dac::DacPin<Ch1>)), | 1358 | (("dac", "OUT1"), quote!(crate::dac::DacPin<Ch1>)), |
| 1356 | (("dac", "OUT2"), quote!(crate::dac::DacPin<Ch2>)), | 1359 | (("dac", "OUT2"), quote!(crate::dac::DacPin<Ch2>)), |
| 1357 | ].into(); | 1360 | ].into(); |
| @@ -1359,9 +1362,22 @@ fn main() { | |||
| 1359 | for p in METADATA.peripherals { | 1362 | for p in METADATA.peripherals { |
| 1360 | if let Some(regs) = &p.registers { | 1363 | if let Some(regs) = &p.registers { |
| 1361 | let mut adc_pairs: BTreeMap<u8, (Option<Ident>, Option<Ident>)> = BTreeMap::new(); | 1364 | let mut adc_pairs: BTreeMap<u8, (Option<Ident>, Option<Ident>)> = BTreeMap::new(); |
| 1365 | let mut seen_lcd_seg_pins = HashSet::new(); | ||
| 1362 | 1366 | ||
| 1363 | for pin in p.pins { | 1367 | for pin in p.pins { |
| 1364 | let key = (regs.kind, pin.signal); | 1368 | let mut key = (regs.kind, pin.signal); |
| 1369 | |||
| 1370 | // LCD is special. There are so many pins! | ||
| 1371 | if regs.kind == "lcd" { | ||
| 1372 | key.1 = pin.signal.trim_end_matches(char::is_numeric); | ||
| 1373 | |||
| 1374 | if key.1 == "SEG" && !seen_lcd_seg_pins.insert(pin.pin) { | ||
| 1375 | // LCD has SEG pins multiplexed in the peripheral | ||
| 1376 | // This means we can see them twice. We need to skip those so we're not impl'ing the trait twice | ||
| 1377 | continue; | ||
| 1378 | } | ||
| 1379 | } | ||
| 1380 | |||
| 1365 | if let Some(tr) = signals.get(&key) { | 1381 | if let Some(tr) = signals.get(&key) { |
| 1366 | let mut peri = format_ident!("{}", p.name); | 1382 | let mut peri = format_ident!("{}", p.name); |
| 1367 | 1383 | ||
diff --git a/embassy-stm32/src/lcd.rs b/embassy-stm32/src/lcd.rs new file mode 100644 index 000000000..ea29f1398 --- /dev/null +++ b/embassy-stm32/src/lcd.rs | |||
| @@ -0,0 +1,510 @@ | |||
| 1 | //! LCD | ||
| 2 | use core::marker::PhantomData; | ||
| 3 | |||
| 4 | use embassy_hal_internal::{Peri, PeripheralType}; | ||
| 5 | |||
| 6 | use crate::gpio::{AfType, AnyPin, SealedPin}; | ||
| 7 | use crate::peripherals; | ||
| 8 | use crate::rcc::{self, RccPeripheral}; | ||
| 9 | use crate::time::Hertz; | ||
| 10 | |||
| 11 | #[cfg(any(stm32u0, stm32l073, stm32l083))] | ||
| 12 | const NUM_SEGMENTS: u8 = 52; | ||
| 13 | #[cfg(any(stm32wb, stm32l4x6, stm32l15x, stm32l162, stm32l4x3, stm32l4x6))] | ||
| 14 | const NUM_SEGMENTS: u8 = 44; | ||
| 15 | #[cfg(any(stm32l053, stm32l063, stm32l100))] | ||
| 16 | const NUM_SEGMENTS: u8 = 32; | ||
| 17 | |||
| 18 | /// LCD configuration struct | ||
| 19 | #[non_exhaustive] | ||
| 20 | #[derive(Debug, Clone, Copy)] | ||
| 21 | pub struct Config { | ||
| 22 | #[cfg(lcd_v2)] | ||
| 23 | /// Enable the voltage output buffer for higher driving capability. | ||
| 24 | /// | ||
| 25 | /// The LCD driving capability is improved as buffers prevent the LCD capacitive loads from loading the resistor | ||
| 26 | /// bridge unacceptably and interfering with its voltage generation. | ||
| 27 | pub use_voltage_output_buffer: bool, | ||
| 28 | /// Enable SEG pin remapping. SEG[31:28] multiplexed with SEG[43:40] | ||
| 29 | pub use_segment_muxing: bool, | ||
| 30 | /// Bias selector | ||
| 31 | pub bias: Bias, | ||
| 32 | /// Duty selector | ||
| 33 | pub duty: Duty, | ||
| 34 | /// Internal or external voltage source | ||
| 35 | pub voltage_source: VoltageSource, | ||
| 36 | /// The frequency used to update the LCD with. | ||
| 37 | /// Should be between ~30 and ~100. Lower is better for power consumption, but has lower visual fidelity. | ||
| 38 | pub target_fps: Hertz, | ||
| 39 | /// LCD driver selector | ||
| 40 | pub drive: Drive, | ||
| 41 | } | ||
| 42 | |||
| 43 | impl Default for Config { | ||
| 44 | fn default() -> Self { | ||
| 45 | Self { | ||
| 46 | #[cfg(lcd_v2)] | ||
| 47 | use_voltage_output_buffer: false, | ||
| 48 | use_segment_muxing: false, | ||
| 49 | bias: Default::default(), | ||
| 50 | duty: Default::default(), | ||
| 51 | voltage_source: Default::default(), | ||
| 52 | target_fps: Hertz(60), | ||
| 53 | drive: Drive::Medium, | ||
| 54 | } | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | /// The number of voltage levels used when driving an LCD. | ||
| 59 | /// Your LCD datasheet should tell you what to use. | ||
| 60 | #[repr(u8)] | ||
| 61 | #[derive(Debug, Default, Clone, Copy)] | ||
| 62 | pub enum Bias { | ||
| 63 | /// 1/4 bias | ||
| 64 | #[default] | ||
| 65 | Quarter = 0b00, | ||
| 66 | /// 1/2 bias | ||
| 67 | Half = 0b01, | ||
| 68 | /// 1/3 bias | ||
| 69 | Third = 0b10, | ||
| 70 | } | ||
| 71 | |||
| 72 | /// The duty used by the LCD driver. | ||
| 73 | /// | ||
| 74 | /// This is essentially how many COM pins you're using. | ||
| 75 | #[repr(u8)] | ||
| 76 | #[derive(Debug, Default, Clone, Copy)] | ||
| 77 | pub enum Duty { | ||
| 78 | #[default] | ||
| 79 | /// Use a single COM pin | ||
| 80 | Static = 0b000, | ||
| 81 | /// Use two COM pins | ||
| 82 | Half = 0b001, | ||
| 83 | /// Use three COM pins | ||
| 84 | Third = 0b010, | ||
| 85 | /// Use four COM pins | ||
| 86 | Quarter = 0b011, | ||
| 87 | /// Use eight COM pins. | ||
| 88 | /// | ||
| 89 | /// In this mode, `COM[7:4]` outputs are available on `SEG[51:48]`. | ||
| 90 | /// This allows reducing the number of available segments. | ||
| 91 | Eigth = 0b100, | ||
| 92 | } | ||
| 93 | |||
| 94 | impl Duty { | ||
| 95 | fn num_com_pins(&self) -> u8 { | ||
| 96 | match self { | ||
| 97 | Duty::Static => 1, | ||
| 98 | Duty::Half => 2, | ||
| 99 | Duty::Third => 3, | ||
| 100 | Duty::Quarter => 4, | ||
| 101 | Duty::Eigth => 8, | ||
| 102 | } | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | /// Whether to use the internal or external voltage source to drive the LCD | ||
| 107 | #[repr(u8)] | ||
| 108 | #[derive(Debug, Default, Clone, Copy)] | ||
| 109 | pub enum VoltageSource { | ||
| 110 | #[default] | ||
| 111 | /// Voltage stepup converter | ||
| 112 | Internal, | ||
| 113 | /// VLCD pin | ||
| 114 | External, | ||
| 115 | } | ||
| 116 | |||
| 117 | /// Defines the pulse duration in terms of ck_ps pulses. | ||
| 118 | /// | ||
| 119 | /// A short pulse leads to lower power consumption, but displays with high internal resistance | ||
| 120 | /// may need a longer pulse to achieve satisfactory contrast. | ||
| 121 | /// Note that the pulse is never longer than one half prescaled LCD clock period. | ||
| 122 | /// | ||
| 123 | /// Displays with high internal resistance may need a longer drive time to achieve satisfactory contrast. | ||
| 124 | /// `PermanentHighDrive` is useful in this case if some additional power consumption can be tolerated. | ||
| 125 | /// | ||
| 126 | /// Basically, for power usage, you want this as low as possible while still being able to use the LCD | ||
| 127 | /// with a good enough contrast. | ||
| 128 | #[repr(u8)] | ||
| 129 | #[derive(Debug, Clone, Copy)] | ||
| 130 | pub enum Drive { | ||
| 131 | /// Zero clock pulse on duration | ||
| 132 | Lowest = 0x00, | ||
| 133 | /// One clock pulse on duration | ||
| 134 | VeryLow = 0x01, | ||
| 135 | /// Two clock pulse on duration | ||
| 136 | Low = 0x02, | ||
| 137 | /// Three clock pulse on duration | ||
| 138 | Medium = 0x03, | ||
| 139 | /// Four clock pulse on duration | ||
| 140 | MediumHigh = 0x04, | ||
| 141 | /// Five clock pulse on duration | ||
| 142 | High = 0x05, | ||
| 143 | /// Six clock pulse on duration | ||
| 144 | VeryHigh = 0x06, | ||
| 145 | /// Seven clock pulse on duration | ||
| 146 | Highest = 0x07, | ||
| 147 | /// Enables the highdrive bit of the hardware | ||
| 148 | PermanentHighDrive = 0x09, | ||
| 149 | } | ||
| 150 | |||
| 151 | /// LCD driver. | ||
| 152 | pub struct Lcd<'d, T: Instance> { | ||
| 153 | _peri: PhantomData<&'d mut T>, | ||
| 154 | duty: Duty, | ||
| 155 | ck_div: u32, | ||
| 156 | } | ||
| 157 | |||
| 158 | impl<'d, T: Instance> Lcd<'d, T> { | ||
| 159 | /// Initialize the lcd driver. | ||
| 160 | /// | ||
| 161 | /// The `pins` parameter must contain *all* segment and com pins that are connected to the LCD. | ||
| 162 | /// This is not further checked by this driver. Pins not routed to the LCD can be used for other purposes. | ||
| 163 | pub fn new<const N: usize>( | ||
| 164 | _peripheral: Peri<'d, T>, | ||
| 165 | config: Config, | ||
| 166 | vlcd_pin: Peri<'_, impl VlcdPin<T>>, | ||
| 167 | pins: [LcdPin<'d, T>; N], | ||
| 168 | ) -> Self { | ||
| 169 | rcc::enable_and_reset::<T>(); | ||
| 170 | |||
| 171 | vlcd_pin.set_as_af( | ||
| 172 | vlcd_pin.af_num(), | ||
| 173 | AfType::output(crate::gpio::OutputType::PushPull, crate::gpio::Speed::VeryHigh), | ||
| 174 | ); | ||
| 175 | |||
| 176 | assert_eq!( | ||
| 177 | pins.iter().filter(|pin| !pin.is_seg).count(), | ||
| 178 | config.duty.num_com_pins() as usize, | ||
| 179 | "The number of provided COM pins is not the same as the duty configures" | ||
| 180 | ); | ||
| 181 | |||
| 182 | // Set the pins | ||
| 183 | for pin in pins { | ||
| 184 | pin.pin.set_as_af( | ||
| 185 | pin.af_num, | ||
| 186 | AfType::output(crate::gpio::OutputType::PushPull, crate::gpio::Speed::VeryHigh), | ||
| 187 | ); | ||
| 188 | } | ||
| 189 | |||
| 190 | // Initialize the display ram to 0 | ||
| 191 | for i in 0..8 { | ||
| 192 | T::regs().ram_com(i).low().write_value(0); | ||
| 193 | T::regs().ram_com(i).high().write_value(0); | ||
| 194 | } | ||
| 195 | |||
| 196 | // Calculate the clock dividers | ||
| 197 | let Some(lcd_clk) = (unsafe { rcc::get_freqs().rtc.to_hertz() }) else { | ||
| 198 | panic!("The LCD driver needs the RTC/LCD clock to be running"); | ||
| 199 | }; | ||
| 200 | let duty_divider = match config.duty { | ||
| 201 | Duty::Static => 1, | ||
| 202 | Duty::Half => 2, | ||
| 203 | Duty::Third => 3, | ||
| 204 | Duty::Quarter => 4, | ||
| 205 | Duty::Eigth => 8, | ||
| 206 | }; | ||
| 207 | let target_clock = config.target_fps.0 * duty_divider; | ||
| 208 | let target_division = lcd_clk.0 / target_clock; | ||
| 209 | |||
| 210 | let mut ps = 0; | ||
| 211 | let mut div = 0; | ||
| 212 | let mut best_fps_match = u32::MAX; | ||
| 213 | |||
| 214 | for trial_div in 0..0xF { | ||
| 215 | let trial_ps = (target_division / (trial_div + 16)) | ||
| 216 | .next_power_of_two() | ||
| 217 | .trailing_zeros(); | ||
| 218 | let fps = lcd_clk.0 / ((1 << trial_ps) * (trial_div + 16)) / duty_divider; | ||
| 219 | |||
| 220 | if fps < config.target_fps.0 { | ||
| 221 | continue; | ||
| 222 | } | ||
| 223 | |||
| 224 | if fps < best_fps_match { | ||
| 225 | ps = trial_ps; | ||
| 226 | div = trial_div; | ||
| 227 | best_fps_match = fps; | ||
| 228 | } | ||
| 229 | } | ||
| 230 | |||
| 231 | let ck_div = lcd_clk.0 / ((1 << ps) * (div + 16)); | ||
| 232 | |||
| 233 | trace!( | ||
| 234 | "lcd_clk: {}, fps: {}, ps: {}, div: {}, ck_div: {}", | ||
| 235 | lcd_clk, best_fps_match, ps, div, ck_div | ||
| 236 | ); | ||
| 237 | |||
| 238 | if best_fps_match == u32::MAX || ps > 0xF { | ||
| 239 | panic!("Lcd clock error"); | ||
| 240 | } | ||
| 241 | |||
| 242 | // Set the frame control | ||
| 243 | T::regs().fcr().modify(|w| { | ||
| 244 | w.set_ps(ps as u8); | ||
| 245 | w.set_div(div as u8); | ||
| 246 | w.set_cc(0b100); // Init in the middle-ish | ||
| 247 | w.set_dead(0b000); | ||
| 248 | w.set_pon(config.drive as u8 & 0x07); | ||
| 249 | w.set_hd((config.drive as u8 & !0x07) != 0); | ||
| 250 | }); | ||
| 251 | |||
| 252 | // Wait for the frame control to synchronize | ||
| 253 | while !T::regs().sr().read().fcrsf() {} | ||
| 254 | |||
| 255 | // Set the control register values | ||
| 256 | T::regs().cr().modify(|w| { | ||
| 257 | #[cfg(lcd_v2)] | ||
| 258 | w.set_bufen(config.use_voltage_output_buffer); | ||
| 259 | w.set_mux_seg(config.use_segment_muxing); | ||
| 260 | w.set_bias(config.bias as u8); | ||
| 261 | w.set_duty(config.duty as u8); | ||
| 262 | w.set_vsel(matches!(config.voltage_source, VoltageSource::External)); | ||
| 263 | }); | ||
| 264 | |||
| 265 | // Enable the lcd | ||
| 266 | T::regs().cr().modify(|w| w.set_lcden(true)); | ||
| 267 | |||
| 268 | // Wait for the lcd to be enabled | ||
| 269 | while !T::regs().sr().read().ens() {} | ||
| 270 | |||
| 271 | // Wait for the stepup converter to be ready | ||
| 272 | while !T::regs().sr().read().rdy() {} | ||
| 273 | |||
| 274 | Self { | ||
| 275 | _peri: PhantomData, | ||
| 276 | duty: config.duty, | ||
| 277 | ck_div, | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | /// Change the contrast by changing the voltage being used. | ||
| 282 | /// | ||
| 283 | /// This is from low at 0 to high at 7. | ||
| 284 | pub fn set_contrast_control(&mut self, value: u8) { | ||
| 285 | assert!((0..=7).contains(&value)); | ||
| 286 | T::regs().fcr().modify(|w| w.set_cc(value)); | ||
| 287 | } | ||
| 288 | |||
| 289 | /// Change the contrast by introducing a deadtime to the signals | ||
| 290 | /// where the voltages are held at 0V. | ||
| 291 | /// | ||
| 292 | /// This is from no dead time at 0 to high dead time at 7. | ||
| 293 | pub fn set_dead_time(&mut self, value: u8) { | ||
| 294 | assert!((0..=7).contains(&value)); | ||
| 295 | T::regs() | ||
| 296 | .fcr() | ||
| 297 | .modify(|w: &mut stm32_metapac::lcd::regs::Fcr| w.set_dead(value)); | ||
| 298 | } | ||
| 299 | |||
| 300 | /// Write data into the display RAM. This overwrites the data already in it for the specified com index. | ||
| 301 | /// | ||
| 302 | /// The `com_index` value determines which part of the RAM is written to. | ||
| 303 | /// The `segments` value is a bitmap where each bit represents whether a pixel is turned on or off. | ||
| 304 | /// | ||
| 305 | /// This function waits last update request to be finished, but does not submit the buffer to the LCD with a new request. | ||
| 306 | /// Submission has to be done manually using [Self::submit_frame]. | ||
| 307 | pub fn write_com_segments(&mut self, com_index: u8, segments: u64) { | ||
| 308 | while T::regs().sr().read().udr() {} | ||
| 309 | |||
| 310 | assert!( | ||
| 311 | com_index < self.duty.num_com_pins(), | ||
| 312 | "Com index cannot be higher than number of configured com pins (through the Duty setting in the config)" | ||
| 313 | ); | ||
| 314 | |||
| 315 | assert!( | ||
| 316 | segments.leading_zeros() >= 64 - self.num_segments() as u32, | ||
| 317 | "Invalid segment pixel set", | ||
| 318 | ); | ||
| 319 | |||
| 320 | T::regs() | ||
| 321 | .ram_com(com_index as usize) | ||
| 322 | .low() | ||
| 323 | .write_value((segments & 0xFFFF_FFFF) as u32); | ||
| 324 | T::regs() | ||
| 325 | .ram_com(com_index as usize) | ||
| 326 | .high() | ||
| 327 | .write_value(((segments >> 32) & 0xFFFF_FFFF) as u32); | ||
| 328 | } | ||
| 329 | |||
| 330 | /// Read the data from the display RAM. | ||
| 331 | /// | ||
| 332 | /// The `com_index` value determines which part of the RAM is read from. | ||
| 333 | /// | ||
| 334 | /// This function waits for the last update request to be finished. | ||
| 335 | pub fn read_com_segments(&self, com_index: u8) -> u64 { | ||
| 336 | while T::regs().sr().read().udr() {} | ||
| 337 | |||
| 338 | assert!( | ||
| 339 | com_index < self.duty.num_com_pins(), | ||
| 340 | "Com index cannot be higher than number of configured com pins (through the Duty setting in the config)" | ||
| 341 | ); | ||
| 342 | |||
| 343 | let low = T::regs().ram_com(com_index as usize).low().read(); | ||
| 344 | let high = T::regs().ram_com(com_index as usize).high().read(); | ||
| 345 | |||
| 346 | ((high as u64) << 32) | low as u64 | ||
| 347 | } | ||
| 348 | |||
| 349 | /// Submit the current RAM data to the LCD. | ||
| 350 | /// | ||
| 351 | /// This function waits until the RAM is writable, but does not wait for the frame to be drawn. | ||
| 352 | pub fn submit_frame(&mut self) { | ||
| 353 | while T::regs().sr().read().udr() {} | ||
| 354 | // Clear the update done flag | ||
| 355 | T::regs().sr().write(|w| w.set_udd(true)); | ||
| 356 | // Set the update request flag | ||
| 357 | T::regs().sr().write(|w| w.set_udr(true)); | ||
| 358 | } | ||
| 359 | |||
| 360 | /// Get the number of segments that are supported on this LCD | ||
| 361 | pub fn num_segments(&self) -> u8 { | ||
| 362 | match self.duty { | ||
| 363 | Duty::Eigth => NUM_SEGMENTS - 4, // With 8 coms, 4 of the segment pins turn into com pins | ||
| 364 | _ => NUM_SEGMENTS, | ||
| 365 | } | ||
| 366 | } | ||
| 367 | |||
| 368 | /// Get the pixel mask for the current LCD setup. | ||
| 369 | /// This is a mask of all bits that are allowed to be set in the [Self::write_com_segments] function. | ||
| 370 | pub fn segment_pixel_mask(&self) -> u64 { | ||
| 371 | (1 << self.num_segments()) - 1 | ||
| 372 | } | ||
| 373 | |||
| 374 | /// Get the number of COM pins that were configured through the Drive config | ||
| 375 | pub fn num_com_pins(&self) -> u8 { | ||
| 376 | self.duty.num_com_pins() | ||
| 377 | } | ||
| 378 | |||
| 379 | /// Set the blink behavior on some pixels. | ||
| 380 | /// | ||
| 381 | /// The blink frequency is an approximation. It's divided from the clock selected by the FPS. | ||
| 382 | /// Play with the FPS value if you want the blink frequency to be more accurate. | ||
| 383 | /// | ||
| 384 | /// If a blink frequency cannot be attained, this function will panic. | ||
| 385 | pub fn set_blink(&mut self, selector: BlinkSelector, freq: BlinkFreq) { | ||
| 386 | // Freq * 100 to be able to do integer math | ||
| 387 | let scaled_blink_freq = match freq { | ||
| 388 | BlinkFreq::Hz0_25 => 25, | ||
| 389 | BlinkFreq::Hz0_5 => 50, | ||
| 390 | BlinkFreq::Hz1 => 100, | ||
| 391 | BlinkFreq::Hz2 => 200, | ||
| 392 | BlinkFreq::Hz4 => 400, | ||
| 393 | }; | ||
| 394 | |||
| 395 | let desired_divider = self.ck_div * 100 / scaled_blink_freq; | ||
| 396 | let target_divider = desired_divider.next_power_of_two(); | ||
| 397 | let power_divisions = target_divider.trailing_zeros(); | ||
| 398 | |||
| 399 | trace!( | ||
| 400 | "Setting LCD blink frequency -> desired_divider: {}, target_divider: {}", | ||
| 401 | desired_divider, target_divider | ||
| 402 | ); | ||
| 403 | |||
| 404 | assert!( | ||
| 405 | (8..=1024).contains(&target_divider), | ||
| 406 | "LCD blink frequency cannot be attained" | ||
| 407 | ); | ||
| 408 | |||
| 409 | T::regs().fcr().modify(|reg| { | ||
| 410 | reg.set_blinkf((power_divisions - 3) as u8); | ||
| 411 | reg.set_blink(selector as u8); | ||
| 412 | }) | ||
| 413 | } | ||
| 414 | } | ||
| 415 | |||
| 416 | impl<'d, T: Instance> Drop for Lcd<'d, T> { | ||
| 417 | fn drop(&mut self) { | ||
| 418 | // Disable the lcd | ||
| 419 | T::regs().cr().modify(|w| w.set_lcden(false)); | ||
| 420 | rcc::disable::<T>(); | ||
| 421 | } | ||
| 422 | } | ||
| 423 | |||
| 424 | /// Blink frequency | ||
| 425 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 426 | pub enum BlinkFreq { | ||
| 427 | /// 0.25 hz | ||
| 428 | Hz0_25, | ||
| 429 | /// 0.5 hz | ||
| 430 | Hz0_5, | ||
| 431 | /// 1 hz | ||
| 432 | Hz1, | ||
| 433 | /// 2 hz | ||
| 434 | Hz2, | ||
| 435 | /// 4 hz | ||
| 436 | Hz4, | ||
| 437 | } | ||
| 438 | |||
| 439 | /// Blink pixel selector | ||
| 440 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 441 | #[repr(u8)] | ||
| 442 | pub enum BlinkSelector { | ||
| 443 | /// No pixels blink | ||
| 444 | None = 0b00, | ||
| 445 | /// The SEG0, COM0 pixel blinks if the pixel is set | ||
| 446 | Seg0Com0 = 0b01, | ||
| 447 | /// The SEG0 pixel of all COMs blinks if the pixel is set | ||
| 448 | Seg0ComAll = 0b10, | ||
| 449 | /// All pixels blink if the pixel is set | ||
| 450 | All = 0b11, | ||
| 451 | } | ||
| 452 | |||
| 453 | /// A type-erased pin that can be configured as an LCD pin. | ||
| 454 | /// This is used for passing pins to the new function in the array. | ||
| 455 | pub struct LcdPin<'d, T: Instance> { | ||
| 456 | pin: Peri<'d, AnyPin>, | ||
| 457 | af_num: u8, | ||
| 458 | is_seg: bool, | ||
| 459 | _phantom: PhantomData<T>, | ||
| 460 | } | ||
| 461 | |||
| 462 | impl<'d, T: Instance> LcdPin<'d, T> { | ||
| 463 | /// Construct an LCD pin from any pin that supports it | ||
| 464 | pub fn new_seg(pin: Peri<'d, impl SegPin<T>>) -> Self { | ||
| 465 | let af = pin.af_num(); | ||
| 466 | |||
| 467 | Self { | ||
| 468 | pin: pin.into(), | ||
| 469 | af_num: af, | ||
| 470 | is_seg: true, | ||
| 471 | _phantom: PhantomData, | ||
| 472 | } | ||
| 473 | } | ||
| 474 | |||
| 475 | /// Construct an LCD pin from any pin that supports it | ||
| 476 | pub fn new_com(pin: Peri<'d, impl ComPin<T>>) -> Self { | ||
| 477 | let af = pin.af_num(); | ||
| 478 | |||
| 479 | Self { | ||
| 480 | pin: pin.into(), | ||
| 481 | af_num: af, | ||
| 482 | is_seg: false, | ||
| 483 | _phantom: PhantomData, | ||
| 484 | } | ||
| 485 | } | ||
| 486 | } | ||
| 487 | |||
| 488 | trait SealedInstance: crate::rcc::SealedRccPeripheral + PeripheralType { | ||
| 489 | fn regs() -> crate::pac::lcd::Lcd; | ||
| 490 | } | ||
| 491 | |||
| 492 | /// DSI instance trait. | ||
| 493 | #[allow(private_bounds)] | ||
| 494 | pub trait Instance: SealedInstance + RccPeripheral + 'static {} | ||
| 495 | |||
| 496 | pin_trait!(SegPin, Instance); | ||
| 497 | pin_trait!(ComPin, Instance); | ||
| 498 | pin_trait!(VlcdPin, Instance); | ||
| 499 | |||
| 500 | foreach_peripheral!( | ||
| 501 | (lcd, $inst:ident) => { | ||
| 502 | impl crate::lcd::SealedInstance for peripherals::$inst { | ||
| 503 | fn regs() -> crate::pac::lcd::Lcd { | ||
| 504 | crate::pac::$inst | ||
| 505 | } | ||
| 506 | } | ||
| 507 | |||
| 508 | impl crate::lcd::Instance for peripherals::$inst {} | ||
| 509 | }; | ||
| 510 | ); | ||
diff --git a/embassy-stm32/src/lib.rs b/embassy-stm32/src/lib.rs index ef6f1d6dc..9e6ba1f34 100644 --- a/embassy-stm32/src/lib.rs +++ b/embassy-stm32/src/lib.rs | |||
| @@ -95,6 +95,8 @@ pub mod i2c; | |||
| 95 | pub mod i2s; | 95 | pub mod i2s; |
| 96 | #[cfg(stm32wb)] | 96 | #[cfg(stm32wb)] |
| 97 | pub mod ipcc; | 97 | pub mod ipcc; |
| 98 | #[cfg(lcd)] | ||
| 99 | pub mod lcd; | ||
| 98 | #[cfg(feature = "low-power")] | 100 | #[cfg(feature = "low-power")] |
| 99 | pub mod low_power; | 101 | pub mod low_power; |
| 100 | #[cfg(lptim)] | 102 | #[cfg(lptim)] |
diff --git a/examples/stm32u0/.cargo/config.toml b/examples/stm32u0/.cargo/config.toml index 688347084..e9212cacb 100644 --- a/examples/stm32u0/.cargo/config.toml +++ b/examples/stm32u0/.cargo/config.toml | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] | 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] |
| 2 | # replace stm32u083rctx with your chip as listed in `probe-rs chip list` | 2 | # replace stm32u083mctx with your chip as listed in `probe-rs chip list` |
| 3 | runner = "probe-rs run --chip stm32u083rctx" | 3 | runner = "probe-rs run --chip stm32u083mctx" |
| 4 | 4 | ||
| 5 | [build] | 5 | [build] |
| 6 | target = "thumbv6m-none-eabi" | 6 | target = "thumbv6m-none-eabi" |
diff --git a/examples/stm32u0/Cargo.toml b/examples/stm32u0/Cargo.toml index 9f5227e3f..8cc894cb3 100644 --- a/examples/stm32u0/Cargo.toml +++ b/examples/stm32u0/Cargo.toml | |||
| @@ -6,8 +6,8 @@ license = "MIT OR Apache-2.0" | |||
| 6 | publish = false | 6 | publish = false |
| 7 | 7 | ||
| 8 | [dependencies] | 8 | [dependencies] |
| 9 | # Change stm32u083rc to your chip name, if necessary. | 9 | # Change stm32u083mc to your chip name, if necessary. |
| 10 | embassy-stm32 = { version = "0.4.0", path = "../../embassy-stm32", features = [ "defmt", "time-driver-any", "stm32u083rc", "memory-x", "unstable-pac", "exti", "chrono"] } | 10 | embassy-stm32 = { version = "0.4.0", path = "../../embassy-stm32", features = [ "defmt", "time-driver-any", "stm32u083mc", "memory-x", "unstable-pac", "exti", "chrono"] } |
| 11 | embassy-sync = { version = "0.7.2", path = "../../embassy-sync", features = ["defmt"] } | 11 | embassy-sync = { version = "0.7.2", path = "../../embassy-sync", features = ["defmt"] } |
| 12 | embassy-executor = { version = "0.9.0", path = "../../embassy-executor", features = ["arch-cortex-m", "executor-thread", "defmt"] } | 12 | embassy-executor = { version = "0.9.0", path = "../../embassy-executor", features = ["arch-cortex-m", "executor-thread", "defmt"] } |
| 13 | embassy-time = { version = "0.5.0", path = "../../embassy-time", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] } | 13 | embassy-time = { version = "0.5.0", path = "../../embassy-time", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] } |
| @@ -19,9 +19,7 @@ defmt-rtt = "1.0.0" | |||
| 19 | 19 | ||
| 20 | cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } | 20 | cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } |
| 21 | cortex-m-rt = "0.7.0" | 21 | cortex-m-rt = "0.7.0" |
| 22 | embedded-hal = "0.2.6" | ||
| 23 | panic-probe = { version = "1.0.0", features = ["print-defmt"] } | 22 | panic-probe = { version = "1.0.0", features = ["print-defmt"] } |
| 24 | heapless = { version = "0.8", default-features = false } | ||
| 25 | 23 | ||
| 26 | micromath = "2.0.0" | 24 | micromath = "2.0.0" |
| 27 | chrono = { version = "0.4.38", default-features = false } | 25 | chrono = { version = "0.4.38", default-features = false } |
diff --git a/examples/stm32u0/src/bin/lcd.rs b/examples/stm32u0/src/bin/lcd.rs new file mode 100644 index 000000000..2b34d4ef1 --- /dev/null +++ b/examples/stm32u0/src/bin/lcd.rs | |||
| @@ -0,0 +1,412 @@ | |||
| 1 | #![no_std] | ||
| 2 | #![no_main] | ||
| 3 | |||
| 4 | use defmt::*; | ||
| 5 | use embassy_executor::Spawner; | ||
| 6 | use embassy_stm32::lcd::{Bias, BlinkFreq, BlinkSelector, Config, Duty, Lcd, LcdPin}; | ||
| 7 | use embassy_stm32::peripherals::LCD; | ||
| 8 | use embassy_stm32::time::Hertz; | ||
| 9 | use embassy_time::Duration; | ||
| 10 | use {defmt_rtt as _, panic_probe as _}; | ||
| 11 | |||
| 12 | #[embassy_executor::main] | ||
| 13 | async fn main(_spawner: Spawner) { | ||
| 14 | let mut config = embassy_stm32::Config::default(); | ||
| 15 | // The RTC clock = the LCD clock and must be running | ||
| 16 | { | ||
| 17 | use embassy_stm32::rcc::*; | ||
| 18 | config.rcc.sys = Sysclk::PLL1_R; | ||
| 19 | config.rcc.hsi = true; | ||
| 20 | config.rcc.pll = Some(Pll { | ||
| 21 | source: PllSource::HSI, // 16 MHz | ||
| 22 | prediv: PllPreDiv::DIV1, | ||
| 23 | mul: PllMul::MUL7, // 16 * 7 = 112 MHz | ||
| 24 | divp: None, | ||
| 25 | divq: None, | ||
| 26 | divr: Some(PllRDiv::DIV2), // 112 / 2 = 56 MHz | ||
| 27 | }); | ||
| 28 | config.rcc.ls = LsConfig::default_lsi(); | ||
| 29 | } | ||
| 30 | |||
| 31 | let p = embassy_stm32::init(config); | ||
| 32 | info!("Hello World!"); | ||
| 33 | |||
| 34 | let mut config = Config::default(); | ||
| 35 | config.bias = Bias::Third; | ||
| 36 | config.duty = Duty::Quarter; | ||
| 37 | config.target_fps = Hertz(100); | ||
| 38 | |||
| 39 | let mut lcd = Lcd::new( | ||
| 40 | p.LCD, | ||
| 41 | config, | ||
| 42 | p.PC3, | ||
| 43 | [ | ||
| 44 | LcdPin::new_com(p.PA8), | ||
| 45 | LcdPin::new_com(p.PA9), | ||
| 46 | LcdPin::new_com(p.PA10), | ||
| 47 | LcdPin::new_seg(p.PB1), | ||
| 48 | LcdPin::new_com(p.PB9), | ||
| 49 | LcdPin::new_seg(p.PB11), | ||
| 50 | LcdPin::new_seg(p.PB14), | ||
| 51 | LcdPin::new_seg(p.PB15), | ||
| 52 | LcdPin::new_seg(p.PC4), | ||
| 53 | LcdPin::new_seg(p.PC5), | ||
| 54 | LcdPin::new_seg(p.PC6), | ||
| 55 | LcdPin::new_seg(p.PC8), | ||
| 56 | LcdPin::new_seg(p.PC9), | ||
| 57 | LcdPin::new_seg(p.PC10), | ||
| 58 | LcdPin::new_seg(p.PC11), | ||
| 59 | LcdPin::new_seg(p.PD8), | ||
| 60 | LcdPin::new_seg(p.PD9), | ||
| 61 | LcdPin::new_seg(p.PD12), | ||
| 62 | LcdPin::new_seg(p.PD13), | ||
| 63 | LcdPin::new_seg(p.PD0), | ||
| 64 | LcdPin::new_seg(p.PD1), | ||
| 65 | LcdPin::new_seg(p.PD3), | ||
| 66 | LcdPin::new_seg(p.PD4), | ||
| 67 | LcdPin::new_seg(p.PD5), | ||
| 68 | LcdPin::new_seg(p.PD6), | ||
| 69 | LcdPin::new_seg(p.PE7), | ||
| 70 | LcdPin::new_seg(p.PE8), | ||
| 71 | LcdPin::new_seg(p.PE9), | ||
| 72 | ], | ||
| 73 | ); | ||
| 74 | |||
| 75 | lcd.set_blink(BlinkSelector::All, BlinkFreq::Hz4); | ||
| 76 | { | ||
| 77 | let mut buffer = DisplayBuffer::new(); | ||
| 78 | for i in 0..4 { | ||
| 79 | buffer.write_colon(i); | ||
| 80 | buffer.write(&mut lcd); | ||
| 81 | embassy_time::Timer::after_millis(200).await; | ||
| 82 | buffer.write_dot(i); | ||
| 83 | buffer.write(&mut lcd); | ||
| 84 | embassy_time::Timer::after_millis(200).await; | ||
| 85 | } | ||
| 86 | for i in 0..4 { | ||
| 87 | buffer.write_bar(i); | ||
| 88 | buffer.write(&mut lcd); | ||
| 89 | embassy_time::Timer::after_millis(200).await; | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | embassy_time::Timer::after_millis(1000).await; | ||
| 94 | |||
| 95 | lcd.set_blink(BlinkSelector::None, BlinkFreq::Hz4); | ||
| 96 | |||
| 97 | const MESSAGE: &str = "Hello embassy people. Hope you like this LCD demo :} "; | ||
| 98 | loop { | ||
| 99 | print_message(MESSAGE, &mut lcd, Duration::from_millis(250)).await; | ||
| 100 | print_message(characters::ALL_CHARS, &mut lcd, Duration::from_millis(500)).await; | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | async fn print_message(message: &str, lcd: &mut Lcd<'_, LCD>, delay: Duration) { | ||
| 105 | let mut display_buffer = DisplayBuffer::new(); | ||
| 106 | |||
| 107 | let mut char_buffer = [' '; 6]; | ||
| 108 | for char in message.chars() { | ||
| 109 | char_buffer.copy_within(1.., 0); | ||
| 110 | char_buffer[5] = char; | ||
| 111 | |||
| 112 | display_buffer.clear(); | ||
| 113 | for (i, char) in char_buffer.iter().enumerate() { | ||
| 114 | display_buffer.write_char(i, *char); | ||
| 115 | } | ||
| 116 | display_buffer.write(lcd); | ||
| 117 | |||
| 118 | embassy_time::Timer::after(delay).await; | ||
| 119 | } | ||
| 120 | } | ||
| 121 | |||
| 122 | /// Display layout for the U0-DK | ||
| 123 | mod display_layout { | ||
| 124 | // Character layout. There are 6 characters, left-to-right | ||
| 125 | // T | ||
| 126 | // ───────── | ||
| 127 | // │ N │ | ||
| 128 | // │ │ │ │ │ | ||
| 129 | // TL │ └┐ │ ┌┘ │ TR | ||
| 130 | // │NW│ │ │NE│ | ||
| 131 | // │ │ │ | ||
| 132 | // W─── ───E | ||
| 133 | // │ │ │ | ||
| 134 | // │SW│ │ │SE│ | ||
| 135 | // BL │ ┌┘ │ └┐ │ BR | ||
| 136 | // │ │ │ │ │ | ||
| 137 | // │ S │ | ||
| 138 | // ───────── | ||
| 139 | // B | ||
| 140 | |||
| 141 | pub const CHAR_N_COM: u8 = 3; | ||
| 142 | pub const CHAR_N_SEG: [u8; 6] = [39, 37, 35, 48, 26, 33]; | ||
| 143 | pub const CHAR_NW_COM: u8 = 3; | ||
| 144 | pub const CHAR_NW_SEG: [u8; 6] = [49, 38, 36, 34, 27, 24]; | ||
| 145 | pub const CHAR_W_COM: u8 = 0; | ||
| 146 | pub const CHAR_W_SEG: [u8; 6] = CHAR_NW_SEG; | ||
| 147 | pub const CHAR_SW_COM: u8 = 2; | ||
| 148 | pub const CHAR_SW_SEG: [u8; 6] = CHAR_NW_SEG; | ||
| 149 | pub const CHAR_S_COM: u8 = 2; | ||
| 150 | pub const CHAR_S_SEG: [u8; 6] = [22, 6, 46, 11, 15, 29]; | ||
| 151 | pub const CHAR_SE_COM: u8 = 3; | ||
| 152 | pub const CHAR_SE_SEG: [u8; 6] = CHAR_S_SEG; | ||
| 153 | pub const CHAR_E_COM: u8 = 0; | ||
| 154 | pub const CHAR_E_SEG: [u8; 6] = [23, 45, 47, 14, 28, 32]; | ||
| 155 | pub const CHAR_NE_COM: u8 = 2; | ||
| 156 | pub const CHAR_NE_SEG: [u8; 6] = CHAR_N_SEG; | ||
| 157 | pub const CHAR_T_COM: u8 = 1; | ||
| 158 | pub const CHAR_T_SEG: [u8; 6] = CHAR_N_SEG; | ||
| 159 | pub const CHAR_TL_COM: u8 = 1; | ||
| 160 | pub const CHAR_TL_SEG: [u8; 6] = CHAR_NW_SEG; | ||
| 161 | pub const CHAR_BL_COM: u8 = 0; | ||
| 162 | pub const CHAR_BL_SEG: [u8; 6] = CHAR_S_SEG; | ||
| 163 | pub const CHAR_B_COM: u8 = 1; | ||
| 164 | pub const CHAR_B_SEG: [u8; 6] = CHAR_S_SEG; | ||
| 165 | pub const CHAR_BR_COM: u8 = 1; | ||
| 166 | pub const CHAR_BR_SEG: [u8; 6] = CHAR_E_SEG; | ||
| 167 | pub const CHAR_TR_COM: u8 = 0; | ||
| 168 | pub const CHAR_TR_SEG: [u8; 6] = CHAR_N_SEG; | ||
| 169 | |||
| 170 | pub const COLON_COM: u8 = 2; | ||
| 171 | pub const COLON_SEG: [u8; 4] = [23, 45, 47, 14]; | ||
| 172 | pub const DOT_COM: u8 = 3; | ||
| 173 | pub const DOT_SEG: [u8; 4] = COLON_SEG; | ||
| 174 | /// COM + SEG, bar from top to bottom | ||
| 175 | pub const BAR: [(u8, u8); 4] = [(2, 28), (3, 28), (2, 32), (3, 32)]; | ||
| 176 | } | ||
| 177 | |||
| 178 | mod characters { | ||
| 179 | use super::CharSegment::{self, *}; | ||
| 180 | |||
| 181 | pub const CHAR_0: &[CharSegment] = &[T, TL, BL, B, BR, TR, NW, SE]; | ||
| 182 | pub const CHAR_1: &[CharSegment] = &[NE, TR, BR]; | ||
| 183 | pub const CHAR_2: &[CharSegment] = &[T, BL, B, TR, E, W]; | ||
| 184 | pub const CHAR_3: &[CharSegment] = &[T, B, BR, TR, E]; | ||
| 185 | pub const CHAR_4: &[CharSegment] = &[TL, BR, TR, E, W]; | ||
| 186 | pub const CHAR_5: &[CharSegment] = &[T, TL, B, BR, E, W]; | ||
| 187 | pub const CHAR_6: &[CharSegment] = &[T, TL, BL, B, BR, E, W]; | ||
| 188 | pub const CHAR_7: &[CharSegment] = &[T, NE, S]; | ||
| 189 | pub const CHAR_8: &[CharSegment] = &[T, TL, BL, B, BR, TR, E, W]; | ||
| 190 | pub const CHAR_9: &[CharSegment] = &[T, TL, BR, TR, E, W]; | ||
| 191 | |||
| 192 | pub const CHAR_COLON: &[CharSegment] = &[N, S]; | ||
| 193 | pub const CHAR_SEMICOLON: &[CharSegment] = &[N, SW]; | ||
| 194 | pub const CHAR_EQUALS: &[CharSegment] = &[E, W, B]; | ||
| 195 | pub const CHAR_SLASH: &[CharSegment] = &[SW, NE]; | ||
| 196 | pub const CHAR_BACKSLASH: &[CharSegment] = &[SE, NW]; | ||
| 197 | pub const CHAR_PLUS: &[CharSegment] = &[N, E, S, W]; | ||
| 198 | pub const CHAR_STAR: &[CharSegment] = &[NE, N, NW, SE, S, SW]; | ||
| 199 | pub const CHAR_QUOTE: &[CharSegment] = &[N]; | ||
| 200 | pub const CHAR_BACKTICK: &[CharSegment] = &[NW]; | ||
| 201 | pub const CHAR_DASH: &[CharSegment] = &[W, E]; | ||
| 202 | pub const CHAR_COMMA: &[CharSegment] = &[SW]; | ||
| 203 | pub const CHAR_DOT: &[CharSegment] = &[S]; | ||
| 204 | pub const CHAR_CURLYOPEN: &[CharSegment] = &[T, NW, W, SW, B]; | ||
| 205 | pub const CHAR_CURLYCLOSE: &[CharSegment] = &[T, NE, E, SE, B]; | ||
| 206 | pub const CHAR_AMPERSAND: &[CharSegment] = &[T, NE, NW, W, BL, B, SE]; | ||
| 207 | |||
| 208 | pub const CHAR_A: &[CharSegment] = &[T, TL, TR, E, W, BL, BR]; | ||
| 209 | pub const CHAR_B: &[CharSegment] = &[T, TR, BR, B, N, S, E]; | ||
| 210 | pub const CHAR_C: &[CharSegment] = &[T, TL, BL, B]; | ||
| 211 | pub const CHAR_D: &[CharSegment] = &[T, TR, BR, B, N, S]; | ||
| 212 | pub const CHAR_E: &[CharSegment] = &[T, TL, BL, B, W]; | ||
| 213 | pub const CHAR_F: &[CharSegment] = &[T, TL, BL, W]; | ||
| 214 | pub const CHAR_G: &[CharSegment] = &[T, TL, BL, B, BR, E]; | ||
| 215 | pub const CHAR_H: &[CharSegment] = &[TL, BL, E, W, TR, BR]; | ||
| 216 | pub const CHAR_I: &[CharSegment] = &[T, N, S, B]; | ||
| 217 | pub const CHAR_J: &[CharSegment] = &[TR, BR, B, BL]; | ||
| 218 | pub const CHAR_K: &[CharSegment] = &[TL, BL, W, NE, SE]; | ||
| 219 | pub const CHAR_L: &[CharSegment] = &[TL, BL, B]; | ||
| 220 | pub const CHAR_M: &[CharSegment] = &[BL, TL, NW, NE, TR, BR]; | ||
| 221 | pub const CHAR_N: &[CharSegment] = &[BL, TL, NW, SE, BR, TR]; | ||
| 222 | pub const CHAR_O: &[CharSegment] = &[T, TL, BL, B, BR, TR]; | ||
| 223 | pub const CHAR_P: &[CharSegment] = &[BL, TL, T, TR, E, W]; | ||
| 224 | pub const CHAR_Q: &[CharSegment] = &[T, TL, BL, B, BR, TR, SE]; | ||
| 225 | pub const CHAR_R: &[CharSegment] = &[BL, TL, T, TR, E, W, SE]; | ||
| 226 | pub const CHAR_S: &[CharSegment] = &[T, NW, E, BR, B]; | ||
| 227 | pub const CHAR_T: &[CharSegment] = &[T, N, S]; | ||
| 228 | pub const CHAR_U: &[CharSegment] = &[TL, BL, B, BR, TR]; | ||
| 229 | pub const CHAR_V: &[CharSegment] = &[TL, BL, SW, NE]; | ||
| 230 | pub const CHAR_W: &[CharSegment] = &[TL, BL, SW, SE, BR, TR]; | ||
| 231 | pub const CHAR_X: &[CharSegment] = &[NE, NW, SE, SW]; | ||
| 232 | pub const CHAR_Y: &[CharSegment] = &[NE, NW, S]; | ||
| 233 | pub const CHAR_Z: &[CharSegment] = &[T, NE, SW, B]; | ||
| 234 | |||
| 235 | pub const CHAR_UNKNOWN: &[CharSegment] = &[N, NW, W, SW, S, SE, E, NE, T, TL, BL, B, BR, TR]; | ||
| 236 | |||
| 237 | pub const ALL_CHARS: &str = | ||
| 238 | "0 1 2 3 4 5 6 7 8 9 : ; = / \\ + * ' ` - , . { } & A B C D E F G H I J K L M N O P Q R S T U V W X Y Z � "; | ||
| 239 | |||
| 240 | pub fn get_char_segments(val: char) -> &'static [CharSegment] { | ||
| 241 | match val { | ||
| 242 | val if val.is_whitespace() => &[], | ||
| 243 | |||
| 244 | '0' => CHAR_0, | ||
| 245 | '1' => CHAR_1, | ||
| 246 | '2' => CHAR_2, | ||
| 247 | '3' => CHAR_3, | ||
| 248 | '4' => CHAR_4, | ||
| 249 | '5' => CHAR_5, | ||
| 250 | '6' => CHAR_6, | ||
| 251 | '7' => CHAR_7, | ||
| 252 | '8' => CHAR_8, | ||
| 253 | '9' => CHAR_9, | ||
| 254 | |||
| 255 | ':' => CHAR_COLON, | ||
| 256 | ';' => CHAR_SEMICOLON, | ||
| 257 | '=' => CHAR_EQUALS, | ||
| 258 | '/' => CHAR_SLASH, | ||
| 259 | '\\' => CHAR_BACKSLASH, | ||
| 260 | '+' => CHAR_PLUS, | ||
| 261 | '*' => CHAR_STAR, | ||
| 262 | '\'' => CHAR_QUOTE, | ||
| 263 | '`' => CHAR_BACKTICK, | ||
| 264 | '-' => CHAR_DASH, | ||
| 265 | ',' => CHAR_COMMA, | ||
| 266 | '.' => CHAR_DOT, | ||
| 267 | '{' => CHAR_CURLYOPEN, | ||
| 268 | '}' => CHAR_CURLYCLOSE, | ||
| 269 | '&' => CHAR_AMPERSAND, | ||
| 270 | |||
| 271 | 'A' | 'a' => CHAR_A, | ||
| 272 | 'B' | 'b' => CHAR_B, | ||
| 273 | 'C' | 'c' => CHAR_C, | ||
| 274 | 'D' | 'd' => CHAR_D, | ||
| 275 | 'E' | 'e' => CHAR_E, | ||
| 276 | 'F' | 'f' => CHAR_F, | ||
| 277 | 'G' | 'g' => CHAR_G, | ||
| 278 | 'H' | 'h' => CHAR_H, | ||
| 279 | 'I' | 'i' => CHAR_I, | ||
| 280 | 'J' | 'j' => CHAR_J, | ||
| 281 | 'K' | 'k' => CHAR_K, | ||
| 282 | 'L' | 'l' => CHAR_L, | ||
| 283 | 'M' | 'm' => CHAR_M, | ||
| 284 | 'N' | 'n' => CHAR_N, | ||
| 285 | 'O' | 'o' => CHAR_O, | ||
| 286 | 'P' | 'p' => CHAR_P, | ||
| 287 | 'Q' | 'q' => CHAR_Q, | ||
| 288 | 'R' | 'r' => CHAR_R, | ||
| 289 | 'S' | 's' => CHAR_S, | ||
| 290 | 'T' | 't' => CHAR_T, | ||
| 291 | 'U' | 'u' => CHAR_U, | ||
| 292 | 'V' | 'v' => CHAR_V, | ||
| 293 | 'W' | 'w' => CHAR_W, | ||
| 294 | 'X' | 'x' => CHAR_X, | ||
| 295 | 'Y' | 'y' => CHAR_Y, | ||
| 296 | 'Z' | 'z' => CHAR_Z, | ||
| 297 | |||
| 298 | _ => CHAR_UNKNOWN, | ||
| 299 | } | ||
| 300 | } | ||
| 301 | } | ||
| 302 | |||
| 303 | pub struct DisplayBuffer { | ||
| 304 | pixels: [u64; 4], | ||
| 305 | } | ||
| 306 | |||
| 307 | impl DisplayBuffer { | ||
| 308 | pub const fn new() -> Self { | ||
| 309 | Self { pixels: [0; 4] } | ||
| 310 | } | ||
| 311 | |||
| 312 | pub fn clear(&mut self) { | ||
| 313 | *self = Self::new(); | ||
| 314 | } | ||
| 315 | |||
| 316 | fn write_char_segment(&mut self, index: usize, value: CharSegment) { | ||
| 317 | defmt::assert!(index < 6); | ||
| 318 | let (com, segments) = value.get_com_seg(); | ||
| 319 | self.pixels[com as usize] |= 1 << segments[index]; | ||
| 320 | } | ||
| 321 | |||
| 322 | pub fn write_char(&mut self, index: usize, val: char) { | ||
| 323 | let segments = characters::get_char_segments(val); | ||
| 324 | |||
| 325 | for segment in segments { | ||
| 326 | self.write_char_segment(index, *segment); | ||
| 327 | } | ||
| 328 | } | ||
| 329 | |||
| 330 | pub fn write(&self, lcd: &mut Lcd<'_, LCD>) { | ||
| 331 | lcd.write_com_segments(0, self.pixels[0]); | ||
| 332 | lcd.write_com_segments(1, self.pixels[1]); | ||
| 333 | lcd.write_com_segments(2, self.pixels[2]); | ||
| 334 | lcd.write_com_segments(3, self.pixels[3]); | ||
| 335 | lcd.submit_frame(); | ||
| 336 | } | ||
| 337 | |||
| 338 | pub fn write_colon(&mut self, index: usize) { | ||
| 339 | defmt::assert!(index < 4); | ||
| 340 | self.pixels[display_layout::COLON_COM as usize] |= 1 << display_layout::COLON_SEG[index]; | ||
| 341 | } | ||
| 342 | |||
| 343 | pub fn write_dot(&mut self, index: usize) { | ||
| 344 | defmt::assert!(index < 4); | ||
| 345 | self.pixels[display_layout::DOT_COM as usize] |= 1 << display_layout::DOT_SEG[index]; | ||
| 346 | } | ||
| 347 | |||
| 348 | pub fn write_bar(&mut self, index: usize) { | ||
| 349 | defmt::assert!(index < 4); | ||
| 350 | let (bar_com, bar_seg) = display_layout::BAR[index]; | ||
| 351 | self.pixels[bar_com as usize] |= 1 << bar_seg; | ||
| 352 | } | ||
| 353 | } | ||
| 354 | |||
| 355 | impl Default for DisplayBuffer { | ||
| 356 | fn default() -> Self { | ||
| 357 | Self::new() | ||
| 358 | } | ||
| 359 | } | ||
| 360 | |||
| 361 | #[derive(Debug, Clone, Copy)] | ||
| 362 | enum CharSegment { | ||
| 363 | /// North | ||
| 364 | N, | ||
| 365 | /// North west | ||
| 366 | NW, | ||
| 367 | /// West | ||
| 368 | W, | ||
| 369 | /// South west | ||
| 370 | SW, | ||
| 371 | /// South | ||
| 372 | S, | ||
| 373 | /// South East | ||
| 374 | SE, | ||
| 375 | /// East | ||
| 376 | E, | ||
| 377 | /// North East | ||
| 378 | NE, | ||
| 379 | /// Top | ||
| 380 | T, | ||
| 381 | /// Top left | ||
| 382 | TL, | ||
| 383 | /// Bottom left | ||
| 384 | BL, | ||
| 385 | /// Bottom | ||
| 386 | B, | ||
| 387 | /// Bottom right | ||
| 388 | BR, | ||
| 389 | /// Top right | ||
| 390 | TR, | ||
| 391 | } | ||
| 392 | |||
| 393 | impl CharSegment { | ||
| 394 | fn get_com_seg(&self) -> (u8, [u8; 6]) { | ||
| 395 | match self { | ||
| 396 | CharSegment::N => (display_layout::CHAR_N_COM, display_layout::CHAR_N_SEG), | ||
| 397 | CharSegment::NW => (display_layout::CHAR_NW_COM, display_layout::CHAR_NW_SEG), | ||
| 398 | CharSegment::W => (display_layout::CHAR_W_COM, display_layout::CHAR_W_SEG), | ||
| 399 | CharSegment::SW => (display_layout::CHAR_SW_COM, display_layout::CHAR_SW_SEG), | ||
| 400 | CharSegment::S => (display_layout::CHAR_S_COM, display_layout::CHAR_S_SEG), | ||
| 401 | CharSegment::SE => (display_layout::CHAR_SE_COM, display_layout::CHAR_SE_SEG), | ||
| 402 | CharSegment::E => (display_layout::CHAR_E_COM, display_layout::CHAR_E_SEG), | ||
| 403 | CharSegment::NE => (display_layout::CHAR_NE_COM, display_layout::CHAR_NE_SEG), | ||
| 404 | CharSegment::T => (display_layout::CHAR_T_COM, display_layout::CHAR_T_SEG), | ||
| 405 | CharSegment::TL => (display_layout::CHAR_TL_COM, display_layout::CHAR_TL_SEG), | ||
| 406 | CharSegment::BL => (display_layout::CHAR_BL_COM, display_layout::CHAR_BL_SEG), | ||
| 407 | CharSegment::B => (display_layout::CHAR_B_COM, display_layout::CHAR_B_SEG), | ||
| 408 | CharSegment::BR => (display_layout::CHAR_BR_COM, display_layout::CHAR_BR_SEG), | ||
| 409 | CharSegment::TR => (display_layout::CHAR_TR_COM, display_layout::CHAR_TR_SEG), | ||
| 410 | } | ||
| 411 | } | ||
| 412 | } | ||
