/* DingusPPC - The Experimental PowerPC Macintosh emulator Copyright (C) 2018-22 divingkatae and maximum (theweirdo) spatium (Contact divingkatae#1017 or powermax#2286 on Discord for more info) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /** High-level VIA-CUDA combo device emulation. */ #include #include #include #include #include #include #include #include #include #include using namespace std; ViaCuda::ViaCuda() { this->name = "ViaCuda"; supports_types(HWCompType::ADB_HOST | HWCompType::I2C_HOST); // VIA reset clears all internal registers to logic 0 // except timers/counters and the shift register // as stated in the 6522 datasheet this->via_regs[VIA_A] = 0; this->via_regs[VIA_B] = 0; this->via_regs[VIA_DIRB] = 0; this->via_regs[VIA_DIRA] = 0; this->via_regs[VIA_IER] = 0; this->via_regs[VIA_ACR] = 0; this->via_regs[VIA_PCR] = 0; this->via_regs[VIA_IFR] = 0; // load maximum value into the timer registers for safety // (not prescribed in the 6522 datasheet) this->via_regs[VIA_T1LL] = 0xFF; this->via_regs[VIA_T1LH] = 0xFF; this->via_regs[VIA_T2CL] = 0xFF; this->via_regs[VIA_T2CH] = 0xFF; this->_via_ifr = 0; // all flags cleared this->_via_ier = 0; // all interrupts disabled this->irq = 0; // IRQ is not active // intialize counters/timers this->t1_counter = 0xFFFF; this->t1_active = false; this->t2_counter = 0xFFFF; this->t2_active = false; // calculate VIA clock duration in ns this->via_clk_dur = 1.0f / VIA_CLOCK_HZ * NS_PER_SEC; // PRAM is part of Cuda this->pram_obj = std::unique_ptr (new NVram("pram.bin", 256)); // ADB bus is driven by Cuda this->adb_bus = std::unique_ptr (new ADB_Bus()); this->cuda_init(); this->int_ctrl = nullptr; } int ViaCuda::device_postinit() { this->int_ctrl = dynamic_cast( gMachineObj->get_comp_by_type(HWCompType::INT_CTRL)); this->irq_id = this->int_ctrl->register_dev_int(IntSrc::VIA_CUDA); return 0; } void ViaCuda::cuda_init() { this->old_tip = 0; this->old_byteack = 0; this->treq = 1; this->in_count = 0; this->out_count = 0; this->poll_rate = 11; } uint8_t ViaCuda::read(int reg) { /* reading from some VIA registers triggers special actions */ switch (reg & 0xF) { case VIA_B: return (this->via_regs[VIA_B]); case VIA_A: case VIA_ANH: LOG_F(WARNING, "Attempted read from VIA Port A!"); break; case VIA_IER: return (this->_via_ier | 0x80); // bit 7 always reads as "1" case VIA_IFR: return this->_via_ifr; case VIA_T1CL: this->_via_ifr &= ~VIA_IF_T1; update_irq(); return this->calc_counter_val(this->t1_counter, this->t1_start_time) & 0xFFU; case VIA_T1CH: return this->calc_counter_val(this->t1_counter, this->t1_start_time) >> 8; case VIA_T2CL: this->_via_ifr &= ~VIA_IF_T2; update_irq(); return this->calc_counter_val(this->t2_counter, this->t2_start_time) & 0xFFU; case VIA_T2CH: return this->calc_counter_val(this->t2_counter, this->t2_start_time) >> 8; case VIA_SR: this->_via_ifr &= ~VIA_IF_SR; update_irq(); break; } return (this->via_regs[reg & 0xF]); } void ViaCuda::write(int reg, uint8_t value) { this->via_regs[reg & 0xF] = value; switch (reg & 0xF) { case VIA_B: write(value); break; case VIA_A: case VIA_ANH: LOG_F(WARNING, "Attempted write to VIA Port A!"); break; case VIA_DIRB: LOG_F(9, "VIA_DIRB = 0x%X", value); break; case VIA_DIRA: LOG_F(9, "VIA_DIRA = 0x%X", value); break; case VIA_PCR: LOG_F(9, "VIA_PCR = 0x%X", value); break; case VIA_ACR: LOG_F(9, "VIA_ACR = 0x%X", value); break; case VIA_IFR: // for each "1" in value clear the corresponding flags this->_via_ifr &= ~value; update_irq(); break; case VIA_IER: if (value & 0x80) { this->_via_ier |= value & 0x7F; } else { this->_via_ier &= ~value; } update_irq(); print_enabled_ints(); break; case VIA_T1CH: if (this->via_regs[VIA_ACR] & 0xC0) { ABORT_F("Unsupported VIA T1 mode, ACR=0x%X", this->via_regs[VIA_ACR]); } // cancel active T1 timer task if (this->t1_active) { TimerManager::get_instance()->cancel_timer(this->t1_timer_id); this->t1_active = false; } // clear T1 flag in IFR this->_via_ifr &= ~VIA_IF_T1; update_irq(); // load initial value into counter 1 this->t1_counter = (value << 8) | this->via_regs[VIA_T1CL]; // TODO: delay for one phase 2 clock // sample current vCPU time and remember it this->t1_start_time = TimerManager::get_instance()->current_time_ns(); // set up timout timer for T1 this->t1_timer_id = TimerManager::get_instance()->add_oneshot_timer( static_cast(this->via_clk_dur * (this->t1_counter + 3) + 0.5f), [this]() { this->assert_t1_int(); } ); this->t1_active = true; break; case VIA_T2CH: if (this->via_regs[VIA_ACR] & 0x20) { ABORT_F("VIA T2 pulse count mode not supported!"); } // cancel active T2 timer task if (this->t2_active) { TimerManager::get_instance()->cancel_timer(this->t2_timer_id); this->t2_active = false; } // clear T2 flag in IFR this->_via_ifr &= ~VIA_IF_T2; update_irq(); // load initial value into counter 2 this->t2_counter = (value << 8) | this->via_regs[VIA_T2CL]; // TODO: delay for one phase 2 clock // sample current vCPU time and remember it this->t2_start_time = TimerManager::get_instance()->current_time_ns(); // set up timeout timer for T2 this->t2_timer_id = TimerManager::get_instance()->add_oneshot_timer( static_cast(this->via_clk_dur * (this->t2_counter + 3) + 0.5f), [this]() { this->assert_t2_int(); } ); this->t2_active = true; break; case VIA_SR: this->_via_ifr &= ~VIA_IF_SR; update_irq(); break; } } uint16_t ViaCuda::calc_counter_val(const uint16_t last_val, const uint64_t& last_time) { // calcualte current counter value based on elapsed time and timer frequency uint64_t cur_time = TimerManager::get_instance()->current_time_ns(); uint32_t diff = (cur_time - last_time) / this->via_clk_dur; return last_val - diff; } void ViaCuda::print_enabled_ints() { const char* via_int_src[] = {"CA2", "CA1", "SR", "CB2", "CB1", "T2", "T1"}; for (int i = 0; i < 7; i++) { if (this->_via_ier & (1 << i)) LOG_F(INFO, "VIA %s interrupt enabled", via_int_src[i]); } } inline void ViaCuda::update_irq() { uint8_t new_irq = !!(this->_via_ifr & this->_via_ier & 0x7F); this->_via_ifr = (this->_via_ifr & 0x7F) | (new_irq << 7); if (new_irq != this->irq) { this->irq = new_irq; this->int_ctrl->ack_int(this->irq_id, new_irq); } } inline bool ViaCuda::ready() { return ((this->via_regs[VIA_DIRB] & 0x38) == 0x30); } void ViaCuda::assert_sr_int() { this->sr_timer_on = false; this->_via_ifr |= VIA_IF_SR; update_irq(); } void ViaCuda::assert_t1_int() { this->_via_ifr |= VIA_IF_T1; this->t1_active = false; update_irq(); } void ViaCuda::assert_t2_int() { this->_via_ifr |= VIA_IF_T2; this->t2_active = false; update_irq(); } void ViaCuda::assert_ctrl_line(ViaLine line) { switch (line) { case ViaLine::CA1: this->_via_ifr |= VIA_IF_CA1; break; case ViaLine::CA2: this->_via_ifr |= VIA_IF_CA2; break; case ViaLine::CB1: this->_via_ifr |= VIA_IF_CB1; break; case ViaLine::CB2: this->_via_ifr |= VIA_IF_CB1; break; default: ABORT_F("Assertion of unknown VIA line requested!"); } update_irq(); } void ViaCuda::schedule_sr_int(uint64_t timeout_ns) { if (this->sr_timer_on) { TimerManager::get_instance()->cancel_timer(this->sr_timer_id); this->sr_timer_on = false; } this->sr_timer_id = TimerManager::get_instance()->add_oneshot_timer( timeout_ns, [this]() { this->assert_sr_int(); }); this->sr_timer_on = true; } void ViaCuda::write(uint8_t new_state) { if (!ready()) { LOG_F(WARNING, "Cuda not ready!"); return; } int new_tip = !!(new_state & CUDA_TIP); int new_byteack = !!(new_state & CUDA_BYTEACK); /* return if there is no state change */ if (new_tip == this->old_tip && new_byteack == this->old_byteack) return; LOG_F(9, "Cuda state changed!"); this->old_tip = new_tip; this->old_byteack = new_byteack; if (new_tip) { if (new_byteack) { this->via_regs[VIA_B] |= CUDA_TREQ; /* negate TREQ */ this->treq = 1; if (this->in_count) { process_packet(); /* start response transaction */ this->via_regs[VIA_B] &= ~CUDA_TREQ; /* assert TREQ */ this->treq = 0; } this->in_count = 0; } else { LOG_F(9, "Cuda: enter sync state"); this->via_regs[VIA_B] &= ~CUDA_TREQ; /* assert TREQ */ this->treq = 0; this->in_count = 0; this->out_count = 0; } // send dummy byte as idle acknowledge or attention //assert_sr_int(); schedule_sr_int(USECS_TO_NSECS(61)); } else { if (this->via_regs[VIA_ACR] & 0x10) { /* data transfer: Host --> Cuda */ if (this->in_count < sizeof(this->in_buf)) { this->in_buf[this->in_count++] = this->via_regs[VIA_SR]; // tell the system we've read the byte after 71 usecs schedule_sr_int(USECS_TO_NSECS(71)); //assert_sr_int(); } else { LOG_F(WARNING, "Cuda input buffer too small. Truncating data!"); } } else { /* data transfer: Cuda --> Host */ (this->*out_handler)(); //assert_sr_int(); // tell the system we've written next byte after 88 usecs schedule_sr_int(USECS_TO_NSECS(88)); } } } /* sends zeros to host ad infinitum */ void ViaCuda::null_out_handler() { this->via_regs[VIA_SR] = 0; } /* sends PRAM content to host ad infinitum */ void ViaCuda::pram_out_handler() { this->via_regs[VIA_SR] = this->pram_obj->read_byte(this->cur_pram_addr++); } /* sends data from out_buf until exhausted, then switches to next_out_handler */ void ViaCuda::out_buf_handler() { if (this->out_pos < this->out_count) { LOG_F(9, "OutBufHandler: sending next byte 0x%X", this->out_buf[this->out_pos]); this->via_regs[VIA_SR] = this->out_buf[this->out_pos++]; } else if (this->is_open_ended) { LOG_F(9, "OutBufHandler: switching to next handler"); this->out_handler = this->next_out_handler; this->next_out_handler = &ViaCuda::null_out_handler; (this->*out_handler)(); } else { LOG_F(9, "Sending last byte"); this->out_count = 0; this->via_regs[VIA_B] |= CUDA_TREQ; /* negate TREQ */ this->treq = 1; } } void ViaCuda::response_header(uint32_t pkt_type, uint32_t pkt_flag) { this->out_buf[0] = pkt_type; this->out_buf[1] = pkt_flag; this->out_buf[2] = this->in_buf[1]; /* copy original cmd */ this->out_count = 3; this->out_pos = 0; this->out_handler = &ViaCuda::out_buf_handler; this->next_out_handler = &ViaCuda::null_out_handler; this->is_open_ended = false; } void ViaCuda::error_response(uint32_t error) { this->out_buf[0] = CUDA_PKT_ERROR; this->out_buf[1] = error; this->out_buf[2] = this->in_buf[0]; this->out_buf[3] = this->in_buf[1]; /* copy original cmd */ this->out_count = 4; this->out_pos = 0; this->out_handler = &ViaCuda::out_buf_handler; this->next_out_handler = &ViaCuda::null_out_handler; this->is_open_ended = false; } void ViaCuda::process_packet() { if (this->in_count < 2) { LOG_F(ERROR, "Cuda: invalid packet (too few data)!"); error_response(CUDA_ERR_BAD_SIZE); return; } switch (this->in_buf[0]) { case CUDA_PKT_ADB: LOG_F(9, "Cuda: ADB packet received \n"); process_adb_command(this->in_buf[1], this->in_count - 2); break; case CUDA_PKT_PSEUDO: LOG_F(9, "Cuda: pseudo command packet received"); LOG_F(9, "Command: 0x%X", this->in_buf[1]); LOG_F(9, "Data count: %d", this->in_count); for (int i = 0; i < this->in_count; i++) { LOG_F(9, "0x%X ,", this->in_buf[i]); } pseudo_command(this->in_buf[1], this->in_count - 2); break; default: LOG_F(ERROR, "Cuda: unsupported packet type = %d", this->in_buf[0]); error_response(CUDA_ERR_BAD_PKT); } } void ViaCuda::process_adb_command(uint8_t cmd_byte, int data_count) { int adb_dev = cmd_byte >> 4; // 2 for keyboard, 3 for mouse int cmd = cmd_byte & 0xF; if (!cmd) { LOG_F(9, "Cuda: ADB SendReset command requested"); response_header(CUDA_PKT_ADB, 0); } else if (cmd == 1) { LOG_F(9, "Cuda: ADB Flush command requested"); response_header(CUDA_PKT_ADB, 0); } else if ((cmd & 0xC) == 8) { LOG_F(9, "Cuda: ADB Listen command requested"); int adb_reg = cmd_byte & 0x3; if (adb_bus->listen(adb_dev, adb_reg)) { response_header(CUDA_PKT_ADB, 0); for (int data_ptr = 0; data_ptr < adb_bus->get_output_len(); data_ptr++) { this->in_buf[(2 + data_ptr)] = adb_bus->get_output_byte(data_ptr); } } else { response_header(CUDA_PKT_ADB, 2); } } else if ((cmd & 0xC) == 0xC) { LOG_F(9, "Cuda: ADB Talk command requested"); response_header(CUDA_PKT_ADB, 0); int adb_reg = cmd_byte & 0x3; if (adb_bus->talk(adb_dev, adb_reg, this->in_buf[2])) { response_header(CUDA_PKT_ADB, 0); } else { response_header(CUDA_PKT_ADB, 2); } } else { LOG_F(ERROR, "Cuda: unsupported ADB command 0x%X", cmd); error_response(CUDA_ERR_BAD_CMD); } } void ViaCuda::pseudo_command(int cmd, int data_count) { uint16_t addr; int i; switch (cmd) { case CUDA_START_STOP_AUTOPOLL: if (this->in_buf[2]) { LOG_F(INFO, "Cuda: autopoll started, rate: %d ms", this->poll_rate); } else { LOG_F(INFO, "Cuda: autopoll stopped"); } response_header(CUDA_PKT_PSEUDO, 0); break; case CUDA_READ_MCU_MEM: addr = READ_WORD_BE_A(&this->in_buf[2]); response_header(CUDA_PKT_PSEUDO, 0); // if starting address is within PRAM region // prepare to transfer PRAM content, othwesise we will send zeroes if (addr >= CUDA_PRAM_START && addr <= CUDA_PRAM_END) { this->cur_pram_addr = addr - CUDA_PRAM_START; this->next_out_handler = &ViaCuda::pram_out_handler; } else if (addr >= CUDA_ROM_START) { // HACK: Cuda ROM dump requsted so let's partially fake it this->out_buf[3] = 0; // empty copyright string WRITE_WORD_BE_A(&this->out_buf[4], 0x0019U); WRITE_WORD_BE_A(&this->out_buf[6], CUDA_FW_VERSION_MAJOR); WRITE_WORD_BE_A(&this->out_buf[8], CUDA_FW_VERSION_MINOR); this->out_count += 7; } this->is_open_ended = true; break; case CUDA_GET_REAL_TIME: response_header(CUDA_PKT_PSEUDO, 0); this->out_buf[2] = (uint8_t)((this->real_time >> 24) & 0xFF); this->out_buf[3] = (uint8_t)((this->real_time >> 16) & 0xFF); this->out_buf[4] = (uint8_t)((this->real_time >> 8) & 0xFF); this->out_buf[5] = (uint8_t)((this->real_time) & 0xFF); break; case CUDA_WRITE_MCU_MEM: addr = READ_WORD_BE_A(&this->in_buf[2]); // if addr is inside PRAM, update PRAM with data from in_buf // otherwise, ignore data in in_buf if (addr >= CUDA_PRAM_START && addr <= CUDA_PRAM_END) { for (i = 0; i < this->in_count - 4; i++) { this->pram_obj->write_byte((addr - CUDA_PRAM_START + i) & 0xFF, this->in_buf[4+i]); } } response_header(CUDA_PKT_PSEUDO, 0); break; case CUDA_READ_PRAM: addr = READ_WORD_BE_A(&this->in_buf[2]); if (addr <= 0xFF) { response_header(CUDA_PKT_PSEUDO, 0); // this command is open-ended so set up the corresponding context this->cur_pram_addr = addr; this->next_out_handler = &ViaCuda::pram_out_handler; this->is_open_ended = true; } else { error_response(CUDA_ERR_BAD_PAR); } break; case CUDA_SET_REAL_TIME: response_header(CUDA_PKT_PSEUDO, 0); this->real_time = ((uint32_t)in_buf[2]) >> 24; this->real_time += ((uint32_t)in_buf[3]) >> 16; this->real_time += ((uint32_t)in_buf[4]) >> 8; this->real_time += ((uint32_t)in_buf[5]); break; case CUDA_WRITE_PRAM: addr = READ_WORD_BE_A(&this->in_buf[2]); if (addr <= 0xFF) { // transfer data from in_buf to PRAM for (i = 0; i < this->in_count - 4; i++) { this->pram_obj->write_byte((addr + i) & 0xFF, this->in_buf[4+i]); } response_header(CUDA_PKT_PSEUDO, 0); } else { error_response(CUDA_ERR_BAD_PAR); } break; case CUDA_FILE_SERVER_FLAG: response_header(CUDA_PKT_PSEUDO, 0); if (this->in_buf[2]) { LOG_F(INFO, "Cuda: File server flag on"); this->file_server = true; } else { LOG_F(INFO, "Cuda: File server flag off"); this->file_server = false; } break; case CUDA_SET_AUTOPOLL_RATE: this->poll_rate = this->in_buf[2]; LOG_F(INFO, "Cuda: autopoll rate set to %d ms", this->poll_rate); response_header(CUDA_PKT_PSEUDO, 0); break; case CUDA_GET_AUTOPOLL_RATE: response_header(CUDA_PKT_PSEUDO, 0); this->out_buf[3] = this->poll_rate; this->out_count++; break; case CUDA_SET_DEVICE_LIST: response_header(CUDA_PKT_PSEUDO, 0); this->device_mask = ((uint16_t)in_buf[2]) >> 8; this->device_mask += ((uint16_t)in_buf[3]); break; case CUDA_GET_DEVICE_LIST: response_header(CUDA_PKT_PSEUDO, 0); this->out_buf[2] = (uint8_t)((this->device_mask >> 8) & 0xFF); this->out_buf[3] = (uint8_t)((this->device_mask) & 0xFF); break; case CUDA_ONE_SECOND_MODE: LOG_F(INFO, "Cuda: One Second Interrupt - Byte Sent: %d", this->in_buf[2]); response_header(CUDA_PKT_PSEUDO, 0); break; case CUDA_READ_WRITE_I2C: response_header(CUDA_PKT_PSEUDO, 0); i2c_simple_transaction(this->in_buf[2], &this->in_buf[3], this->in_count - 3); break; case CUDA_COMB_FMT_I2C: response_header(CUDA_PKT_PSEUDO, 0); if (this->in_count >= 5) { i2c_comb_transaction( this->in_buf[2], this->in_buf[3], this->in_buf[4], &this->in_buf[5], this->in_count - 5); } break; case CUDA_OUT_PB0: /* undocumented call! */ LOG_F(INFO, "Cuda: send %d to PB0", (int)(this->in_buf[2])); response_header(CUDA_PKT_PSEUDO, 0); break; case CUDA_WARM_START: case CUDA_POWER_DOWN: case CUDA_MONO_STABLE_RESET: case CUDA_RESTART_SYSTEM: /* really kludge temp code */ LOG_F(INFO, "Cuda: Restart/Shutdown signal sent with command 0x%x! \n", cmd); //exit(0); break; default: LOG_F(ERROR, "Cuda: unsupported pseudo command 0x%X", cmd); error_response(CUDA_ERR_BAD_CMD); } } /* sends data from the current I2C to host ad infinitum */ void ViaCuda::i2c_handler() { this->receive_byte(this->curr_i2c_addr, &this->via_regs[VIA_SR]); } void ViaCuda::i2c_simple_transaction(uint8_t dev_addr, const uint8_t* in_buf, int in_bytes) { int op_type = dev_addr & 1; /* 0 - write to device, 1 - read from device */ dev_addr >>= 1; /* strip RD/WR bit */ if (!this->start_transaction(dev_addr)) { LOG_F(WARNING, "Unsupported I2C device 0x%X", dev_addr); error_response(CUDA_ERR_I2C); return; } /* send data to the target I2C device until there is no more data to send or the target device doesn't acknowledge that indicates an error */ for (int i = 0; i < in_bytes; i++) { if (!this->send_byte(dev_addr, in_buf[i])) { LOG_F(WARNING, "NO_ACK during sending, device 0x%X", dev_addr); error_response(CUDA_ERR_I2C); return; } } if (op_type) { /* read request initiate an open ended transaction */ this->curr_i2c_addr = dev_addr; this->out_handler = &ViaCuda::out_buf_handler; this->next_out_handler = &ViaCuda::i2c_handler; this->is_open_ended = true; } } void ViaCuda::i2c_comb_transaction( uint8_t dev_addr, uint8_t sub_addr, uint8_t dev_addr1, const uint8_t* in_buf, int in_bytes) { int op_type = dev_addr1 & 1; /* 0 - write to device, 1 - read from device */ if ((dev_addr & 0xFE) != (dev_addr1 & 0xFE)) { LOG_F(ERROR, "Combined I2C: dev_addr mismatch!"); error_response(CUDA_ERR_I2C); return; } dev_addr >>= 1; /* strip RD/WR bit */ if (!this->start_transaction(dev_addr)) { LOG_F(WARNING, "Unsupported I2C device 0x%X", dev_addr); error_response(CUDA_ERR_I2C); return; } if (!this->send_subaddress(dev_addr, sub_addr)) { LOG_F(WARNING, "NO_ACK while sending subaddress, device 0x%X", dev_addr); error_response(CUDA_ERR_I2C); return; } /* send data to the target I2C device until there is no more data to send or the target device doesn't acknowledge that indicates an error */ for (int i = 0; i < in_bytes; i++) { if (!this->send_byte(dev_addr, in_buf[i])) { LOG_F(WARNING, "NO_ACK during sending, device 0x%X", dev_addr); error_response(CUDA_ERR_I2C); return; } } if (!op_type) { /* return dummy response for writes */ LOG_F(WARNING, "Combined I2C - write request!"); } else { this->curr_i2c_addr = dev_addr; this->out_handler = &ViaCuda::out_buf_handler; this->next_out_handler = &ViaCuda::i2c_handler; this->is_open_ended = true; } } static const DeviceDescription ViaCuda_Descriptor = { ViaCuda::create, {}, {} }; REGISTER_DEVICE(ViaCuda, ViaCuda_Descriptor);