1//! LTDC - LCD-TFT Display Controller
2//! See ST application note AN4861: Introduction to LCD-TFT display controller (LTDC) on STM32 MCUs for high level details
3//! This module was tested against the stm32h735g-dk using the RM0468 ST reference manual for detailed register information
4
5use core::future::poll_fn;
6use core::marker::PhantomData;
7use core::task::Poll;
8
9use embassy_hal_internal::{into_ref, PeripheralRef};
10use embassy_sync::waitqueue::AtomicWaker;
11use stm32_metapac::ltdc::regs::Dccr;
12use stm32_metapac::ltdc::vals::{Bf1, Bf2, Cfuif, Clif, Crrif, Cterrif, Pf, Vbr};
13
14use crate::gpio::{AfType, OutputType, Speed};
15use crate::interrupt::typelevel::Interrupt;
16use crate::interrupt::{self};
17use crate::{peripherals, rcc, Peripheral};
18
19static LTDC_WAKER: AtomicWaker = AtomicWaker::new();
20
21/// LTDC error
22#[derive(Debug, PartialEq, Eq, Clone, Copy)]
23#[cfg_attr(feature = "defmt", derive(defmt::Format))]
24pub enum Error {
25 /// FIFO underrun. Generated when a pixel is requested while the FIFO is empty
26 FifoUnderrun,
27 /// Transfer error. Generated when a bus error occurs
28 TransferError,
29}
30
31/// Display configuration parameters
32#[derive(Clone, Copy, Debug, PartialEq)]
33#[cfg_attr(feature = "defmt", derive(defmt::Format))]
34pub struct LtdcConfiguration {
35 /// Active width in pixels
36 pub active_width: u16,
37 /// Active height in pixels
38 pub active_height: u16,
39
40 /// Horizontal back porch (in units of pixel clock period)
41 pub h_back_porch: u16,
42 /// Horizontal front porch (in units of pixel clock period)
43 pub h_front_porch: u16,
44 /// Vertical back porch (in units of horizontal scan line)
45 pub v_back_porch: u16,
46 /// Vertical front porch (in units of horizontal scan line)
47 pub v_front_porch: u16,
48
49 /// Horizontal synchronization width (in units of pixel clock period)
50 pub h_sync: u16,
51 /// Vertical synchronization height (in units of horizontal scan line)
52 pub v_sync: u16,
53
54 /// Horizontal synchronization polarity: `false`: active low, `true`: active high
55 pub h_sync_polarity: PolarityActive,
56 /// Vertical synchronization polarity: `false`: active low, `true`: active high
57 pub v_sync_polarity: PolarityActive,
58 /// Data enable polarity: `false`: active low, `true`: active high
59 pub data_enable_polarity: PolarityActive,
60 /// Pixel clock polarity: `false`: falling edge, `true`: rising edge
61 pub pixel_clock_polarity: PolarityEdge,
62}
63
64/// Edge polarity
65#[derive(Clone, Copy, Debug, PartialEq)]
66#[cfg_attr(feature = "defmt", derive(defmt::Format))]
67pub enum PolarityEdge {
68 /// Falling edge
69 FallingEdge,
70 /// Rising edge
71 RisingEdge,
72}
73
74/// Active polarity
75#[derive(Clone, Copy, Debug, PartialEq)]
76#[cfg_attr(feature = "defmt", derive(defmt::Format))]
77pub enum PolarityActive {
78 /// Active low
79 ActiveLow,
80 /// Active high
81 ActiveHigh,
82}
83
84/// LTDC driver.
85pub struct Ltdc<'d, T: Instance> {
86 _peri: PeripheralRef<'d, T>,
87}
88
89/// LTDC interrupt handler.
90pub struct InterruptHandler<T: Instance> {
91 _phantom: PhantomData<T>,
92}
93
94/// 24 bit color
95#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
96#[cfg_attr(feature = "defmt", derive(defmt::Format))]
97pub struct RgbColor {
98 /// Red
99 pub red: u8,
100 /// Green
101 pub green: u8,
102 /// Blue
103 pub blue: u8,
104}
105
106/// Layer
107#[derive(Debug, PartialEq, Eq, Clone, Copy)]
108#[cfg_attr(feature = "defmt", derive(defmt::Format))]
109pub struct LtdcLayerConfig {
110 /// Layer number
111 pub layer: LtdcLayer,
112 /// Pixel format
113 pub pixel_format: PixelFormat,
114 /// Window left x in pixels
115 pub window_x0: u16,
116 /// Window right x in pixels
117 pub window_x1: u16,
118 /// Window top y in pixels
119 pub window_y0: u16,
120 /// Window bottom y in pixels
121 pub window_y1: u16,
122}
123
124/// Pixel format
125#[repr(u8)]
126#[derive(Debug, PartialEq, Eq, Clone, Copy)]
127#[cfg_attr(feature = "defmt", derive(defmt::Format))]
128pub enum PixelFormat {
129 /// ARGB8888
130 ARGB8888 = Pf::ARGB8888 as u8,
131 /// RGB888
132 RGB888 = Pf::RGB888 as u8,
133 /// RGB565
134 RGB565 = Pf::RGB565 as u8,
135 /// ARGB1555
136 ARGB1555 = Pf::ARGB1555 as u8,
137 /// ARGB4444
138 ARGB4444 = Pf::ARGB4444 as u8,
139 /// L8 (8-bit luminance)
140 L8 = Pf::L8 as u8,
141 /// AL44 (4-bit alpha, 4-bit luminance
142 AL44 = Pf::AL44 as u8,
143 /// AL88 (8-bit alpha, 8-bit luminance)
144 AL88 = Pf::AL88 as u8,
145}
146
147impl PixelFormat {
148 /// Number of bytes per pixel
149 pub fn bytes_per_pixel(&self) -> usize {
150 match self {
151 PixelFormat::ARGB8888 => 4,
152 PixelFormat::RGB888 => 3,
153 PixelFormat::RGB565 | PixelFormat::ARGB4444 | PixelFormat::ARGB1555 | PixelFormat::AL88 => 2,
154 PixelFormat::AL44 | PixelFormat::L8 => 1,
155 }
156 }
157}
158
159/// Ltdc Blending Layer
160#[repr(usize)]
161#[derive(Debug, PartialEq, Eq, Clone, Copy)]
162#[cfg_attr(feature = "defmt", derive(defmt::Format))]
163pub enum LtdcLayer {
164 /// Bottom layer
165 Layer1 = 0,
166 /// Top layer
167 Layer2 = 1,
168}
169
170impl<T: Instance> interrupt::typelevel::Handler<T::Interrupt> for InterruptHandler<T> {
171 unsafe fn on_interrupt() {
172 cortex_m::asm::dsb();
173 Ltdc::<T>::enable_interrupts(enable:false);
174 LTDC_WAKER.wake();
175 }
176}
177
178impl<'d, T: Instance> Ltdc<'d, T> {
179 // Create a new LTDC driver without specifying color and control pins. This is typically used if you want to drive a display though a DsiHost
180 /// Note: Full-Duplex modes are not supported at this time
181 pub fn new(peri: impl Peripheral<P = T> + 'd) -> Self {
182 Self::setup_clocks();
183 into_ref!(peri);
184 Self { _peri: peri }
185 }
186
187 /// Create a new LTDC driver. 8 pins per color channel for blue, green and red
188 #[allow(clippy::too_many_arguments)]
189 pub fn new_with_pins(
190 peri: impl Peripheral<P = T> + 'd,
191 _irq: impl interrupt::typelevel::Binding<T::Interrupt, InterruptHandler<T>> + 'd,
192 clk: impl Peripheral<P = impl ClkPin<T>> + 'd,
193 hsync: impl Peripheral<P = impl HsyncPin<T>> + 'd,
194 vsync: impl Peripheral<P = impl VsyncPin<T>> + 'd,
195 b0: impl Peripheral<P = impl B0Pin<T>> + 'd,
196 b1: impl Peripheral<P = impl B1Pin<T>> + 'd,
197 b2: impl Peripheral<P = impl B2Pin<T>> + 'd,
198 b3: impl Peripheral<P = impl B3Pin<T>> + 'd,
199 b4: impl Peripheral<P = impl B4Pin<T>> + 'd,
200 b5: impl Peripheral<P = impl B5Pin<T>> + 'd,
201 b6: impl Peripheral<P = impl B6Pin<T>> + 'd,
202 b7: impl Peripheral<P = impl B7Pin<T>> + 'd,
203 g0: impl Peripheral<P = impl G0Pin<T>> + 'd,
204 g1: impl Peripheral<P = impl G1Pin<T>> + 'd,
205 g2: impl Peripheral<P = impl G2Pin<T>> + 'd,
206 g3: impl Peripheral<P = impl G3Pin<T>> + 'd,
207 g4: impl Peripheral<P = impl G4Pin<T>> + 'd,
208 g5: impl Peripheral<P = impl G5Pin<T>> + 'd,
209 g6: impl Peripheral<P = impl G6Pin<T>> + 'd,
210 g7: impl Peripheral<P = impl G7Pin<T>> + 'd,
211 r0: impl Peripheral<P = impl R0Pin<T>> + 'd,
212 r1: impl Peripheral<P = impl R1Pin<T>> + 'd,
213 r2: impl Peripheral<P = impl R2Pin<T>> + 'd,
214 r3: impl Peripheral<P = impl R3Pin<T>> + 'd,
215 r4: impl Peripheral<P = impl R4Pin<T>> + 'd,
216 r5: impl Peripheral<P = impl R5Pin<T>> + 'd,
217 r6: impl Peripheral<P = impl R6Pin<T>> + 'd,
218 r7: impl Peripheral<P = impl R7Pin<T>> + 'd,
219 ) -> Self {
220 Self::setup_clocks();
221 into_ref!(peri);
222 new_pin!(clk, AfType::output(OutputType::PushPull, Speed::VeryHigh));
223 new_pin!(hsync, AfType::output(OutputType::PushPull, Speed::VeryHigh));
224 new_pin!(vsync, AfType::output(OutputType::PushPull, Speed::VeryHigh));
225 new_pin!(b0, AfType::output(OutputType::PushPull, Speed::VeryHigh));
226 new_pin!(b1, AfType::output(OutputType::PushPull, Speed::VeryHigh));
227 new_pin!(b2, AfType::output(OutputType::PushPull, Speed::VeryHigh));
228 new_pin!(b3, AfType::output(OutputType::PushPull, Speed::VeryHigh));
229 new_pin!(b4, AfType::output(OutputType::PushPull, Speed::VeryHigh));
230 new_pin!(b5, AfType::output(OutputType::PushPull, Speed::VeryHigh));
231 new_pin!(b6, AfType::output(OutputType::PushPull, Speed::VeryHigh));
232 new_pin!(b7, AfType::output(OutputType::PushPull, Speed::VeryHigh));
233 new_pin!(g0, AfType::output(OutputType::PushPull, Speed::VeryHigh));
234 new_pin!(g1, AfType::output(OutputType::PushPull, Speed::VeryHigh));
235 new_pin!(g2, AfType::output(OutputType::PushPull, Speed::VeryHigh));
236 new_pin!(g3, AfType::output(OutputType::PushPull, Speed::VeryHigh));
237 new_pin!(g4, AfType::output(OutputType::PushPull, Speed::VeryHigh));
238 new_pin!(g5, AfType::output(OutputType::PushPull, Speed::VeryHigh));
239 new_pin!(g6, AfType::output(OutputType::PushPull, Speed::VeryHigh));
240 new_pin!(g7, AfType::output(OutputType::PushPull, Speed::VeryHigh));
241 new_pin!(r0, AfType::output(OutputType::PushPull, Speed::VeryHigh));
242 new_pin!(r1, AfType::output(OutputType::PushPull, Speed::VeryHigh));
243 new_pin!(r2, AfType::output(OutputType::PushPull, Speed::VeryHigh));
244 new_pin!(r3, AfType::output(OutputType::PushPull, Speed::VeryHigh));
245 new_pin!(r4, AfType::output(OutputType::PushPull, Speed::VeryHigh));
246 new_pin!(r5, AfType::output(OutputType::PushPull, Speed::VeryHigh));
247 new_pin!(r6, AfType::output(OutputType::PushPull, Speed::VeryHigh));
248 new_pin!(r7, AfType::output(OutputType::PushPull, Speed::VeryHigh));
249
250 Self { _peri: peri }
251 }
252
253 /// Initialise and enable the display
254 pub fn init(&mut self, config: &LtdcConfiguration) {
255 use stm32_metapac::ltdc::vals::{Depol, Hspol, Pcpol, Vspol};
256 let ltdc = T::regs();
257
258 // check bus access
259 assert!(ltdc.gcr().read().0 == 0x2220); // reset value
260
261 // configure the HS, VS, DE and PC polarity
262 ltdc.gcr().modify(|w| {
263 w.set_hspol(match config.h_sync_polarity {
264 PolarityActive::ActiveHigh => Hspol::ACTIVE_HIGH,
265 PolarityActive::ActiveLow => Hspol::ACTIVE_LOW,
266 });
267
268 w.set_vspol(match config.v_sync_polarity {
269 PolarityActive::ActiveHigh => Vspol::ACTIVE_HIGH,
270 PolarityActive::ActiveLow => Vspol::ACTIVE_LOW,
271 });
272
273 w.set_depol(match config.data_enable_polarity {
274 PolarityActive::ActiveHigh => Depol::ACTIVE_HIGH,
275 PolarityActive::ActiveLow => Depol::ACTIVE_LOW,
276 });
277
278 w.set_pcpol(match config.pixel_clock_polarity {
279 PolarityEdge::RisingEdge => Pcpol::RISING_EDGE,
280 PolarityEdge::FallingEdge => Pcpol::FALLING_EDGE,
281 });
282 });
283
284 // set synchronization pulse width
285 ltdc.sscr().modify(|w| {
286 w.set_vsh(config.v_sync - 1);
287 w.set_hsw(config.h_sync - 1);
288 });
289
290 // set accumulated back porch
291 ltdc.bpcr().modify(|w| {
292 w.set_avbp(config.v_sync + config.v_back_porch - 1);
293 w.set_ahbp(config.h_sync + config.h_back_porch - 1);
294 });
295
296 // set accumulated active width
297 let aa_height = config.v_sync + config.v_back_porch + config.active_height - 1;
298 let aa_width = config.h_sync + config.h_back_porch + config.active_width - 1;
299 ltdc.awcr().modify(|w| {
300 w.set_aah(aa_height);
301 w.set_aaw(aa_width);
302 });
303
304 // set total width and height
305 let total_height: u16 = config.v_sync + config.v_back_porch + config.active_height + config.v_front_porch - 1;
306 let total_width: u16 = config.h_sync + config.h_back_porch + config.active_width + config.h_front_porch - 1;
307 ltdc.twcr().modify(|w| {
308 w.set_totalh(total_height);
309 w.set_totalw(total_width)
310 });
311
312 // set the background color value to black
313 ltdc.bccr().modify(|w| {
314 w.set_bcred(0);
315 w.set_bcgreen(0);
316 w.set_bcblue(0);
317 });
318
319 self.enable();
320 }
321
322 /// Set the enable bit in the control register and assert that it has been enabled
323 ///
324 /// This does need to be called if init has already been called
325 pub fn enable(&mut self) {
326 T::regs().gcr().modify(|w| w.set_ltdcen(true));
327 assert!(T::regs().gcr().read().ltdcen())
328 }
329
330 /// Unset the enable bit in the control register and assert that it has been disabled
331 pub fn disable(&mut self) {
332 T::regs().gcr().modify(|w| w.set_ltdcen(false));
333 assert!(!T::regs().gcr().read().ltdcen())
334 }
335
336 /// Initialise and enable the layer
337 ///
338 /// clut - a 256 length color look-up table applies to L8, AL44 and AL88 pixel format and will default to greyscale if `None` supplied and these pixel formats are used
339 pub fn init_layer(&mut self, layer_config: &LtdcLayerConfig, clut: Option<&[RgbColor]>) {
340 let ltdc = T::regs();
341 let layer = ltdc.layer(layer_config.layer as usize);
342
343 // 256 color look-up table for L8, AL88 and AL88 pixel formats
344 if let Some(clut) = clut {
345 assert_eq!(clut.len(), 256, "Color lookup table must be exactly 256 in length");
346 for (index, color) in clut.iter().enumerate() {
347 layer.clutwr().write(|w| {
348 w.set_clutadd(index as u8);
349 w.set_red(color.red);
350 w.set_green(color.green);
351 w.set_blue(color.blue);
352 });
353 }
354 }
355
356 // configure the horizontal start and stop position
357 let h_win_start = layer_config.window_x0 + ltdc.bpcr().read().ahbp() + 1;
358 let h_win_stop = layer_config.window_x1 + ltdc.bpcr().read().ahbp();
359 layer.whpcr().write(|w| {
360 w.set_whstpos(h_win_start);
361 w.set_whsppos(h_win_stop);
362 });
363
364 // configure the vertical start and stop position
365 let v_win_start = layer_config.window_y0 + ltdc.bpcr().read().avbp() + 1;
366 let v_win_stop = layer_config.window_y1 + ltdc.bpcr().read().avbp();
367 layer.wvpcr().write(|w| {
368 w.set_wvstpos(v_win_start);
369 w.set_wvsppos(v_win_stop)
370 });
371
372 // set the pixel format
373 layer
374 .pfcr()
375 .write(|w| w.set_pf(Pf::from_bits(layer_config.pixel_format as u8)));
376
377 // set the default color value to transparent black
378 layer.dccr().write_value(Dccr::default());
379
380 // set the global constant alpha value
381 let alpha = 0xFF;
382 layer.cacr().write(|w| w.set_consta(alpha));
383
384 // set the blending factors.
385 layer.bfcr().modify(|w| {
386 w.set_bf1(Bf1::PIXEL);
387 w.set_bf2(Bf2::PIXEL);
388 });
389
390 // calculate framebuffer pixel size in bytes
391 let bytes_per_pixel = layer_config.pixel_format.bytes_per_pixel() as u16;
392 let width = layer_config.window_x1 - layer_config.window_x0;
393 let height = layer_config.window_y1 - layer_config.window_y0;
394
395 // framebuffer pitch and line length
396 layer.cfblr().modify(|w| {
397 w.set_cfbp(width * bytes_per_pixel);
398 #[cfg(not(stm32u5))]
399 w.set_cfbll(width * bytes_per_pixel + 7);
400 #[cfg(stm32u5)]
401 w.set_cfbll(width * bytes_per_pixel + 3);
402 });
403
404 // framebuffer line number
405 layer.cfblnr().modify(|w| w.set_cfblnbr(height));
406
407 // enable LTDC_Layer by setting LEN bit
408 layer.cr().modify(|w| {
409 if clut.is_some() {
410 w.set_cluten(true);
411 }
412 w.set_len(true);
413 });
414 }
415
416 /// Set the current buffer. The async function will return when buffer has been completely copied to the LCD screen
417 /// frame_buffer_addr is a pointer to memory that should not move (best to make it static)
418 pub async fn set_buffer(&mut self, layer: LtdcLayer, frame_buffer_addr: *const ()) -> Result<(), Error> {
419 let mut bits = T::regs().isr().read();
420
421 // if all clear
422 if !bits.fuif() && !bits.lif() && !bits.rrif() && !bits.terrif() {
423 // wait for interrupt
424 poll_fn(|cx| {
425 // quick check to avoid registration if already done.
426 let bits = T::regs().isr().read();
427 if bits.fuif() || bits.lif() || bits.rrif() || bits.terrif() {
428 return Poll::Ready(());
429 }
430
431 LTDC_WAKER.register(cx.waker());
432 Self::clear_interrupt_flags(); // don't poison the request with old flags
433 Self::enable_interrupts(true);
434
435 // set the new frame buffer address
436 let layer = T::regs().layer(layer as usize);
437 layer.cfbar().modify(|w| w.set_cfbadd(frame_buffer_addr as u32));
438
439 // configure a shadow reload for the next blanking period
440 T::regs().srcr().write(|w| {
441 w.set_vbr(Vbr::RELOAD);
442 });
443
444 // need to check condition after register to avoid a race
445 // condition that would result in lost notifications.
446 let bits = T::regs().isr().read();
447 if bits.fuif() || bits.lif() || bits.rrif() || bits.terrif() {
448 Poll::Ready(())
449 } else {
450 Poll::Pending
451 }
452 })
453 .await;
454
455 // re-read the status register after wait.
456 bits = T::regs().isr().read();
457 }
458
459 let result = if bits.fuif() {
460 Err(Error::FifoUnderrun)
461 } else if bits.terrif() {
462 Err(Error::TransferError)
463 } else if bits.lif() {
464 panic!("line interrupt event is disabled")
465 } else if bits.rrif() {
466 // register reload flag is expected
467 Ok(())
468 } else {
469 unreachable!("all interrupt status values checked")
470 };
471
472 Self::clear_interrupt_flags();
473 result
474 }
475
476 fn setup_clocks() {
477 critical_section::with(|_cs| {
478 // RM says the pllsaidivr should only be changed when pllsai is off. But this could have other unintended side effects. So let's just give it a try like this.
479 // According to the debugger, this bit gets set, anyway.
480 #[cfg(stm32f7)]
481 crate::pac::RCC
482 .dckcfgr1()
483 .modify(|w| w.set_pllsaidivr(stm32_metapac::rcc::vals::Pllsaidivr::DIV2));
484
485 // It is set to RCC_PLLSAIDIVR_2 in ST's BSP example for the STM32469I-DISCO.
486 #[cfg(stm32f4)]
487 crate::pac::RCC
488 .dckcfgr()
489 .modify(|w| w.set_pllsaidivr(stm32_metapac::rcc::vals::Pllsaidivr::DIV2));
490 });
491
492 rcc::enable_and_reset::<T>();
493 }
494
495 fn clear_interrupt_flags() {
496 T::regs().icr().write(|w| {
497 w.set_cfuif(Cfuif::CLEAR);
498 w.set_clif(Clif::CLEAR);
499 w.set_crrif(Crrif::CLEAR);
500 w.set_cterrif(Cterrif::CLEAR);
501 });
502 }
503
504 fn enable_interrupts(enable: bool) {
505 T::regs().ier().write(|w| {
506 w.set_fuie(enable);
507 w.set_lie(false); // we are not interested in the line interrupt enable event
508 w.set_rrie(enable);
509 w.set_terrie(enable)
510 });
511
512 // enable interrupts for LTDC peripheral
513 T::Interrupt::unpend();
514 if enable {
515 unsafe { T::Interrupt::enable() };
516 } else {
517 T::Interrupt::disable()
518 }
519 }
520}
521
522impl<'d, T: Instance> Drop for Ltdc<'d, T> {
523 fn drop(&mut self) {}
524}
525
526trait SealedInstance: crate::rcc::SealedRccPeripheral {
527 fn regs() -> crate::pac::ltdc::Ltdc;
528}
529
530/// LTDC instance trait.
531#[allow(private_bounds)]
532pub trait Instance: SealedInstance + Peripheral<P = Self> + crate::rcc::RccPeripheral + 'static + Send {
533 /// Interrupt for this LTDC instance.
534 type Interrupt: interrupt::typelevel::Interrupt;
535}
536
537pin_trait!(ClkPin, Instance);
538pin_trait!(HsyncPin, Instance);
539pin_trait!(VsyncPin, Instance);
540pin_trait!(DePin, Instance);
541pin_trait!(R0Pin, Instance);
542pin_trait!(R1Pin, Instance);
543pin_trait!(R2Pin, Instance);
544pin_trait!(R3Pin, Instance);
545pin_trait!(R4Pin, Instance);
546pin_trait!(R5Pin, Instance);
547pin_trait!(R6Pin, Instance);
548pin_trait!(R7Pin, Instance);
549pin_trait!(G0Pin, Instance);
550pin_trait!(G1Pin, Instance);
551pin_trait!(G2Pin, Instance);
552pin_trait!(G3Pin, Instance);
553pin_trait!(G4Pin, Instance);
554pin_trait!(G5Pin, Instance);
555pin_trait!(G6Pin, Instance);
556pin_trait!(G7Pin, Instance);
557pin_trait!(B0Pin, Instance);
558pin_trait!(B1Pin, Instance);
559pin_trait!(B2Pin, Instance);
560pin_trait!(B3Pin, Instance);
561pin_trait!(B4Pin, Instance);
562pin_trait!(B5Pin, Instance);
563pin_trait!(B6Pin, Instance);
564pin_trait!(B7Pin, Instance);
565
566foreach_interrupt!(
567 ($inst:ident, ltdc, LTDC, GLOBAL, $irq:ident) => {
568 impl Instance for peripherals::$inst {
569 type Interrupt = crate::interrupt::typelevel::$irq;
570 }
571
572 impl SealedInstance for peripherals::$inst {
573 fn regs() -> crate::pac::ltdc::Ltdc {
574 crate::pac::$inst
575 }
576 }
577 };
578);
579