chipset_legacy/winbond83977_sio/
super_io.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! This file implements the emulation of the Winbond 83977 super I/O (SIO)
5//! chipset.
6//!
7//! The chipset is accessed via I/O ports that are normally used for the floppy
8//! controller. As such, there is a dependency between the floppy controller
9//! device and this device. This module doesn't install its own I/O port
10//! callbacks. Rather, it relies on the floppy controller module to call it when
11//! the I/O ports are accessed.
12//!
13//! # Emulation accuracy
14//!
15//! There is a universe where we went off the deep-end and actually supported
16//! each and every little bit and bob of configuration data exposed by the
17//! chipset, which includes wacky stuff like relocating the address of
18//! fundamental devices (like the RTC), changing IRQ modes, etc...
19//!
20//! Thankfully, we don't live in that universe.
21//!
22//! Instead, we just hard code some sane defaults, sanity check some writes, and
23//! hope that no-one tried to do anything particularly exotic.
24//!
25//! And hey, it's been working great since '02, so why stop now?
26//!
27//! The alternative would be a _lot_ of effort, and a _lot_ of cross-device
28//! plumbing, which isn't really something we'd like to be in the business of
29//! doing (let alone for an archaic device this this).
30
31use chipset_device::ChipsetDevice;
32use inspect::Inspect;
33use inspect::InspectMut;
34use open_enum::open_enum;
35use thiserror::Error;
36use vmcore::device_state::ChangeDeviceState;
37
38const NUM_SIO_DEVICES: usize = 9;
39
40#[derive(Debug, Error)]
41pub enum SioConfigError {
42    #[error("sio controller not in config mode: {0:?} state")]
43    NotInConfigMode(ConfigIdxState),
44}
45
46#[derive(Default, Debug, Inspect, Copy, Clone)]
47#[inspect(debug)]
48pub enum ConfigIdxState {
49    /// Waiting for first byte of handshake.
50    #[default]
51    Idle,
52    /// Waiting for second byte of handshake.
53    Handshake,
54    /// Handshake complete.
55    Ready,
56}
57
58open_enum! {
59    /// Configuration Registers - See Section 10. of the spec.
60    #[derive(Default, Inspect)]
61    #[inspect(debug)]
62    enum ConfigRegister: u8 {
63        // Card-specific registers
64        LOGICAL_DEVICE_NUMBER = 0x07,
65        DEVICE_ID             = 0x20,
66        REVISION_NUMBER       = 0x21,
67        POWER_DOWN_CONTROL    = 0x22,
68        PNP_CONTROL           = 0x24,
69
70        // Device-specific registers.
71        ENABLE_DEVICE = 0x30,
72        IO_BASE_MSB0  = 0x60,
73        IO_BASE_LSB0  = 0x61,
74        IO_BASE_MSB1  = 0x62,
75        IO_BASE_LSB1  = 0x63,
76
77        IRQ_SELECT1 = 0x70,
78        IRQ_TYPE1   = 0x71,
79        IRQ_SELECT2 = 0x72,
80        IRQ_TYPE2   = 0x73,
81        DMA_CONFIG1 = 0x74,
82        DMA_CONFIG2 = 0x75,
83
84        ADDRESS_UNDOCUMENTED = 0xBA,
85
86        DEVICE_BIT_CONFIG0 = 0xE8,
87        DEVICE_BIT_CONFIG1 = 0xE9,
88        DEVICE_BIT_CONFIG2 = 0xEA,
89        DEVICE_BIT_CONFIG3 = 0xEB,
90        DEVICE_BIT_CONFIG4 = 0xEC,
91        DEVICE_BIT_CONFIG5 = 0xED,
92        DEVICE_BIT_CONFIG6 = 0xEE,
93        DEVICE_BIT_CONFIG7 = 0xEF,
94
95        DEVICE_CONFIG0 = 0xF0,
96        DEVICE_CONFIG1 = 0xF1,
97        DEVICE_CONFIG2 = 0xF2,
98        DEVICE_CONFIG3 = 0xF3,
99        DEVICE_CONFIG4 = 0xF4,
100        DEVICE_CONFIG5 = 0xF5,
101    }
102}
103
104impl ConfigRegister {
105    /// Check if this register contains device-specific data.
106    fn is_device_specific(&self) -> bool {
107        self.0 >= 0x30
108    }
109}
110
111open_enum! {
112    #[derive(Default, Inspect)]
113    #[inspect(debug)]
114    enum LogicalDeviceIndex: u8 {
115        FLOPPY_CONTROLLER   = 0,
116        PARALLEL_PORT       = 1,
117        COM1_PORT           = 2,
118        COM2_PORT           = 3,
119        RTC                 = 4,
120        KEYBOARD_CONTROLLER = 5,
121        INFRARED_PORT       = 6,
122        AUX_IO_CONTROL1     = 7,
123        AUX_IO_CONTROL2     = 8,
124    }
125}
126
127#[derive(Debug, Default, Copy, Clone, Inspect)]
128struct LogicalDeviceData {
129    enabled: bool,
130    #[inspect(hex, iter_by_index)]
131    io_port_base: [u16; 2],
132    #[inspect(bytes)]
133    irq_vector: [u8; 2],
134    #[inspect(bytes)]
135    dma_channel: [u8; 2],
136    // DEVNOTE: For all intents and purposes, you can consider these values
137    // "magic". If you're really interested in what they do (on a per-device
138    // level), feel free to whip out the spec.
139    #[inspect(bytes)]
140    config_data: [u8; 8],
141}
142
143impl LogicalDeviceData {
144    // DEVNOTE: From a "code correctness" POV, all this config data _should_ be
145    // plumbed through via the device's constructor, and reflect the reality of
146    // how the system topology was set up in the top level VMM init code.
147    //
148    // ...but given that this device is only really here to support compatibility
149    // with legacy Hyper-V Generation 1 VMs (which have a rigid system topology
150    // least wrt these sorts of base chipset devices), we'll take the pragmatic
151    // approach of hard-coding these values to "known good" values, and assume the
152    // top-level VMM code hasn't decided to move things around.
153    fn default_data() -> [Self; NUM_SIO_DEVICES] {
154        let mut defaults: [Self; NUM_SIO_DEVICES] = [Self::default(); NUM_SIO_DEVICES];
155
156        defaults[LogicalDeviceIndex::FLOPPY_CONTROLLER.0 as usize] = Self {
157            enabled: true,
158            io_port_base: [0x3F0, 0x370], // [primary, secondary]
159            irq_vector: [6, 0],
160            dma_channel: [2, 0],
161            config_data: [0x0E, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00],
162        };
163
164        // This must exist for compatibility sake, even though we don't actually
165        // have an emulated parallel port.
166        defaults[LogicalDeviceIndex::PARALLEL_PORT.0 as usize] = Self {
167            enabled: false,
168            io_port_base: [0x0000; 2],
169            irq_vector: [0, 0],
170            dma_channel: [4, 0],
171            config_data: [0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
172        };
173
174        defaults[LogicalDeviceIndex::COM1_PORT.0 as usize] = Self {
175            enabled: true,
176            io_port_base: [0x3F8, 0],
177            irq_vector: [3, 0],
178            dma_channel: [4, 0],
179            config_data: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
180        };
181
182        defaults[LogicalDeviceIndex::COM2_PORT.0 as usize] = Self {
183            enabled: true,
184            io_port_base: [0x2F8, 0],
185            irq_vector: [4, 0],
186            dma_channel: [4, 0],
187            config_data: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
188        };
189
190        defaults[LogicalDeviceIndex::RTC.0 as usize] = Self {
191            enabled: true,
192            io_port_base: [0x70, 0],
193            irq_vector: [8, 0],
194            dma_channel: [4, 0],
195            config_data: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
196        };
197
198        defaults[LogicalDeviceIndex::KEYBOARD_CONTROLLER.0 as usize] = Self {
199            enabled: true,
200            io_port_base: [0x60, 0x64], // [keyboard, mouse]
201            irq_vector: [1, 12],        // [keyboard, mouse]
202            dma_channel: [4, 0],
203            config_data: [0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
204        };
205
206        defaults[LogicalDeviceIndex::INFRARED_PORT.0 as usize] = Self {
207            enabled: false,
208            io_port_base: [0x0000; 2],
209            irq_vector: [0, 0],
210            dma_channel: [4, 4],
211            config_data: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
212        };
213
214        defaults[LogicalDeviceIndex::AUX_IO_CONTROL1.0 as usize] = Self {
215            enabled: false,
216            io_port_base: [0x0000; 2],
217            irq_vector: [0, 0],
218            dma_channel: [4, 0],
219            config_data: [0xEF, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00],
220        };
221
222        defaults[LogicalDeviceIndex::AUX_IO_CONTROL2.0 as usize] = Self {
223            enabled: false,
224            io_port_base: [0x0000; 2],
225            irq_vector: [0, 0],
226            dma_channel: [4, 4],
227            config_data: [0xEF, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00],
228        };
229
230        defaults
231    }
232}
233
234#[derive(Debug, Inspect)]
235struct SioControllerState {
236    config_idx_state: ConfigIdxState,
237    config_idx: ConfigRegister,
238    device_idx: LogicalDeviceIndex,
239    #[inspect(iter_by_index)]
240    device_data: [LogicalDeviceData; NUM_SIO_DEVICES],
241}
242
243#[derive(Debug, InspectMut)]
244pub struct SioController {
245    // Volatile state
246    state: SioControllerState,
247}
248
249impl Default for SioController {
250    fn default() -> Self {
251        Self {
252            state: SioControllerState {
253                config_idx_state: ConfigIdxState::default(),
254                config_idx: ConfigRegister::default(),
255                device_idx: LogicalDeviceIndex::default(),
256                device_data: LogicalDeviceData::default_data(),
257            },
258        }
259    }
260}
261
262impl SioController {
263    pub fn update_config_state(&mut self, value: u8) {
264        let prev_state = self.state.config_idx_state;
265
266        // This port provides a handshake for entering SIO config mode.
267        // First check for the secret handshake to enter config mode.
268
269        // 0x87h must be written twice to the Extended Functions Enable Register
270        // (EFER, I/O port address 3F0h or 370h) in order to read or write the
271        // config registers. After programming of config registers is finished
272        // 0xAA should be written to EFER to exit config mode.
273
274        self.state.config_idx_state = match (self.state.config_idx_state, value) {
275            // If in config mode, a write of 0xAA exits config mode.
276            (ConfigIdxState::Ready, 0xAA) => ConfigIdxState::Idle,
277            // Okay, we're in config mode. Pass through the value.
278            (ConfigIdxState::Ready, _) => {
279                // If in config mode, writing to this port sets which config register
280                // gets accessed via `config_{read,write}`.
281                self.state.config_idx = ConfigRegister(value);
282                // remain in the ready state
283                ConfigIdxState::Ready
284            }
285            // If outside of config mode, 0x87 must be written twice in a row to enter config mode.
286            (ConfigIdxState::Idle, 0x87) => ConfigIdxState::Handshake,
287            (ConfigIdxState::Handshake, 0x87) => ConfigIdxState::Ready,
288            // Any other values reset the handshake back to it's default state.
289            (_, _) => ConfigIdxState::Idle,
290        };
291
292        tracing::trace!(
293            ?value,
294            ?prev_state,
295            cur_state = ?self.state.config_idx_state,
296            register = ?self.state.config_idx,
297            "update sio config state"
298        );
299    }
300
301    pub fn config_read(&mut self) -> Result<u8, SioConfigError> {
302        // If not in config mode, reads will abort config handshake.
303        if !matches!(self.state.config_idx_state, ConfigIdxState::Ready) {
304            let state = self.state.config_idx_state;
305            self.state.config_idx_state = ConfigIdxState::Idle;
306            return Err(SioConfigError::NotInConfigMode(state));
307        }
308
309        // Check if this is a read from a non-device-specific register.
310        if !self.state.config_idx.is_device_specific() {
311            let value = match self.state.config_idx {
312                ConfigRegister::DEVICE_ID => 0x97,
313                ConfigRegister::REVISION_NUMBER => 0x71,
314                _ => {
315                    tracelimit::warn_ratelimited!(
316                        ?self.state.config_idx,
317                        "unexpected config register read"
318                    );
319                    0x00
320                }
321            };
322            return Ok(value);
323        }
324
325        let dev = match self
326            .state
327            .device_data
328            .get_mut(self.state.device_idx.0 as usize)
329        {
330            Some(dev) => dev,
331            None => {
332                tracelimit::warn_ratelimited!(
333                    logical_device_number = self.state.device_idx.0,
334                    "invalid logical device index"
335                );
336
337                return Ok(0);
338            }
339        };
340
341        // Handle reads to device-specific registers.
342        let value = match self.state.config_idx {
343            ConfigRegister::ENABLE_DEVICE => dev.enabled as u8,
344            ConfigRegister::IO_BASE_MSB0 => (dev.io_port_base[0] >> 8) as u8,
345            ConfigRegister::IO_BASE_LSB0 => dev.io_port_base[0] as u8,
346            ConfigRegister::IO_BASE_MSB1 => (dev.io_port_base[1] >> 8) as u8,
347            ConfigRegister::IO_BASE_LSB1 => dev.io_port_base[1] as u8,
348            ConfigRegister::IRQ_SELECT1 => dev.irq_vector[0],
349            ConfigRegister::IRQ_TYPE1 => 0b10, // high, edge triggered
350            ConfigRegister::IRQ_SELECT2 => dev.irq_vector[1],
351            ConfigRegister::IRQ_TYPE2 => 0b10, // high, edge triggered
352            ConfigRegister::DMA_CONFIG1 => dev.dma_channel[0],
353            ConfigRegister::DMA_CONFIG2 => dev.dma_channel[1],
354            ConfigRegister::DEVICE_CONFIG0 => dev.config_data[0],
355            ConfigRegister::DEVICE_CONFIG1 => dev.config_data[1],
356            ConfigRegister::DEVICE_CONFIG2 => dev.config_data[2],
357            ConfigRegister::DEVICE_CONFIG3 => dev.config_data[3],
358            ConfigRegister::DEVICE_CONFIG4 => dev.config_data[4],
359            ConfigRegister::DEVICE_CONFIG5 => dev.config_data[5],
360            _ => {
361                tracelimit::warn_ratelimited!(
362                    ?self.state.config_idx,
363                    "unexpected config register read"
364                );
365                0x00
366            }
367        };
368
369        tracing::trace!(
370            config_reg = ?self.state.config_idx,
371            device_idx = ?self.state.device_idx,
372            ?value,
373            "sio config read"
374        );
375
376        Ok(value)
377    }
378
379    pub fn config_write(&mut self, value: u8) {
380        // If not in config mode, writes will abort config handshake.
381        if !matches!(self.state.config_idx_state, ConfigIdxState::Ready) {
382            self.state.config_idx_state = ConfigIdxState::Idle;
383            return;
384        }
385
386        tracing::trace!(
387            config_reg = ?self.state.config_idx,
388            device_idx = ?self.state.device_idx,
389            ?value,
390            "sio config write"
391        );
392
393        // Check if this is a write to a non-device-specific register.
394        if !self.state.config_idx.is_device_specific() {
395            match self.state.config_idx {
396                ConfigRegister::LOGICAL_DEVICE_NUMBER => {
397                    self.state.device_idx = LogicalDeviceIndex(value);
398                }
399                ConfigRegister::POWER_DOWN_CONTROL => {
400                    if value != 0xFF {
401                        tracelimit::warn_ratelimited!(
402                            value = value,
403                            "invalid value written to POWER_DOWN_CONTROL register"
404                        )
405                    }
406                }
407                ConfigRegister::PNP_CONTROL => {
408                    if value != 0xC4 {
409                        tracelimit::warn_ratelimited!(
410                            value = value,
411                            "invalid value written to PNP_CONTROL register"
412                        )
413                    }
414                }
415                _ => {
416                    tracelimit::warn_ratelimited!(
417                        ?self.state.config_idx,
418                        ?value,
419                        "unexpected config register write"
420                    )
421                }
422            }
423            return;
424        }
425
426        let dev = match self
427            .state
428            .device_data
429            .get_mut(self.state.device_idx.0 as usize)
430        {
431            Some(dev) => dev,
432            None => {
433                tracelimit::warn_ratelimited!(
434                    logical_device_number = self.state.device_idx.0,
435                    "invalid logical device index"
436                );
437
438                return;
439            }
440        };
441
442        // Handle writes to device-specific registers.
443        match self.state.config_idx {
444            ConfigRegister::ENABLE_DEVICE => {
445                // Disallow enabling parallel port.
446                if self.state.device_idx != LogicalDeviceIndex::PARALLEL_PORT {
447                    dev.enabled = value & 0x1 == 1;
448                } else {
449                    tracing::debug!("attempted to enable parallel port")
450                }
451            }
452            ConfigRegister::IO_BASE_MSB0 => {
453                dev.io_port_base[0] = (dev.io_port_base[0] & !0xFF00) | (value as u16) << 8;
454            }
455            ConfigRegister::IO_BASE_LSB0 => {
456                dev.io_port_base[0] = (dev.io_port_base[0] & !0x00FF) | (value as u16);
457            }
458            ConfigRegister::IO_BASE_MSB1 => {
459                dev.io_port_base[1] = (dev.io_port_base[1] & !0xFF00) | (value as u16) << 8;
460            }
461            ConfigRegister::IO_BASE_LSB1 => {
462                dev.io_port_base[1] = (dev.io_port_base[1] & !0x00FF) | (value as u16);
463            }
464            ConfigRegister::IRQ_SELECT1 => dev.irq_vector[0] = value,
465            ConfigRegister::IRQ_SELECT2 => dev.irq_vector[1] = value,
466            ConfigRegister::DMA_CONFIG1 => dev.dma_channel[0] = value,
467            ConfigRegister::DMA_CONFIG2 => dev.dma_channel[1] = value,
468            ConfigRegister::DEVICE_CONFIG0 => dev.config_data[0] = value,
469            ConfigRegister::DEVICE_CONFIG1 => dev.config_data[1] = value,
470            ConfigRegister::DEVICE_CONFIG2 => dev.config_data[2] = value,
471            ConfigRegister::DEVICE_CONFIG3 => dev.config_data[3] = value,
472            ConfigRegister::DEVICE_CONFIG4 => dev.config_data[4] = value,
473            ConfigRegister::DEVICE_CONFIG5 => dev.config_data[5] = value,
474            _ => {
475                // sanity-check the guest is writing values we support to the
476                // `AUX_IO_CONTROL2`-specific registers
477                if self.state.device_idx == LogicalDeviceIndex::AUX_IO_CONTROL2 {
478                    let expected = match self.state.config_idx {
479                        ConfigRegister::DEVICE_BIT_CONFIG0 => value == 0x10 || value == 0x12, // Keyboard reset functionality
480                        ConfigRegister::DEVICE_BIT_CONFIG5 => value == 0x08, //  A20 Gate functionality
481                        ConfigRegister::ADDRESS_UNDOCUMENTED => value == 0xf0, // Keyboard functionality
482                        _ => false,
483                    };
484
485                    if !expected {
486                        tracelimit::warn_ratelimited!(?self.state.config_idx, ?value, "wrote an unexpected value");
487                    }
488                } else {
489                    tracelimit::warn_ratelimited!(
490                        ?self.state.config_idx,
491                        ?value,
492                        "unexpected config register write"
493                    )
494                }
495            }
496        }
497    }
498}
499
500impl ChangeDeviceState for SioController {
501    fn start(&mut self) {}
502
503    async fn stop(&mut self) {}
504
505    async fn reset(&mut self) {
506        self.state.config_idx_state = ConfigIdxState::default();
507        self.state.config_idx = ConfigRegister::default();
508        self.state.device_idx = LogicalDeviceIndex::default();
509        self.state.device_data = LogicalDeviceData::default_data();
510    }
511}
512
513// Sio is an interesting chipset device, since it doesn't *directly* interact
514// with any chipset services. Rather, it is a sub-component of another chipset
515// device (the winbond83977_sio).
516//
517// Nonetheless, we implement ChipsetDevice for consistency, as it is "logically"
518// a chipset device.
519impl ChipsetDevice for SioController {}
520
521mod save_restore {
522    use super::*;
523    use vmcore::save_restore::RestoreError;
524    use vmcore::save_restore::SaveError;
525    use vmcore::save_restore::SaveRestore;
526
527    mod state {
528        use mesh::payload::Protobuf;
529        use vmcore::save_restore::SavedStateRoot;
530
531        const SAVED_NUM_SIO_DEVICES: usize = 9;
532
533        #[derive(Protobuf)]
534        #[mesh(package = "chipset.superio")]
535        pub enum SavedConfigIdxState {
536            #[mesh(1)]
537            Idle,
538            #[mesh(2)]
539            Handshake,
540            #[mesh(3)]
541            Ready,
542        }
543
544        #[derive(Protobuf)]
545        #[mesh(package = "chipset.superio")]
546        pub struct SavedLogicalDeviceData {
547            #[mesh(1)]
548            pub enabled: bool,
549            #[mesh(2)]
550            pub io_port_base: [u16; 2],
551            #[mesh(3)]
552            pub irq_vector: [u8; 2],
553            #[mesh(4)]
554            pub dma_channel: [u8; 2],
555            #[mesh(5)]
556            pub config_data: [u8; 8],
557        }
558
559        #[derive(Protobuf, SavedStateRoot)]
560        #[mesh(package = "chipset.superio")]
561        pub struct SavedState {
562            #[mesh(1)]
563            pub config_idx_state: SavedConfigIdxState,
564            #[mesh(2)]
565            pub config_idx: u8,
566            #[mesh(3)]
567            pub device_idx: u8,
568            #[mesh(4)]
569            pub device_data: [SavedLogicalDeviceData; SAVED_NUM_SIO_DEVICES],
570        }
571    }
572
573    impl SaveRestore for SioController {
574        type SavedState = state::SavedState;
575
576        fn save(&mut self) -> Result<Self::SavedState, SaveError> {
577            let SioControllerState {
578                config_idx_state,
579                config_idx,
580                device_idx,
581                device_data,
582            } = self.state;
583
584            let saved_state = state::SavedState {
585                config_idx_state: match config_idx_state {
586                    ConfigIdxState::Idle => state::SavedConfigIdxState::Idle,
587                    ConfigIdxState::Handshake => state::SavedConfigIdxState::Handshake,
588                    ConfigIdxState::Ready => state::SavedConfigIdxState::Ready,
589                },
590                config_idx: config_idx.0,
591                device_idx: device_idx.0,
592                device_data: device_data.map(|data| {
593                    let LogicalDeviceData {
594                        enabled,
595                        io_port_base,
596                        irq_vector,
597                        dma_channel,
598                        config_data,
599                    } = data;
600
601                    state::SavedLogicalDeviceData {
602                        enabled,
603                        io_port_base,
604                        irq_vector,
605                        dma_channel,
606                        config_data,
607                    }
608                }),
609            };
610
611            Ok(saved_state)
612        }
613
614        fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
615            let state::SavedState {
616                config_idx_state,
617                config_idx,
618                device_idx,
619                device_data,
620            } = state;
621
622            self.state = SioControllerState {
623                config_idx_state: match config_idx_state {
624                    state::SavedConfigIdxState::Idle => ConfigIdxState::Idle,
625                    state::SavedConfigIdxState::Handshake => ConfigIdxState::Handshake,
626                    state::SavedConfigIdxState::Ready => ConfigIdxState::Ready,
627                },
628                config_idx: ConfigRegister(config_idx),
629                device_idx: LogicalDeviceIndex(device_idx),
630                device_data: device_data.map(|data| {
631                    let state::SavedLogicalDeviceData {
632                        enabled,
633                        io_port_base,
634                        irq_vector,
635                        dma_channel,
636                        config_data,
637                    } = data;
638
639                    LogicalDeviceData {
640                        enabled,
641                        io_port_base,
642                        irq_vector,
643                        dma_channel,
644                        config_data,
645                    }
646                }),
647            };
648
649            Ok(())
650        }
651    }
652}