mirror of
https://github.com/dingusdev/dingusppc.git
synced 2025-01-09 06:30:51 +00:00
52f4d847dd
We may be transferring less data than can fit in a chunk, so we need to ensure that xfer_cnt is clamped to 0 when transferring the last chunk (otherwise it remains negative, and has_data() will return true). It's also possible that the transfer size is bigger than a chunk but not an even multiple of the chunk size, so we need to ensure that we don't try to transfer a whole chunk in the last iteration. More correctly initialize the device identification struct, to report the maximum (word 47) and current (word 59) number of blocks that can be transferred with READ_MULTIPLE and WRITE_MULTIPLE commands. Fix post_xfer_action to write the actual data size that was written, as opposed to an entire chunk (which may be larger).
341 lines
12 KiB
C++
341 lines
12 KiB
C++
/*
|
|
DingusPPC - The Experimental PowerPC Macintosh emulator
|
|
Copyright (C) 2018-24 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/** @file ATA hard disk emulation. */
|
|
|
|
#include <devices/common/ata/atahd.h>
|
|
#include <devices/deviceregistry.h>
|
|
#include <devices/common/ata/idechannel.h>
|
|
#include <loguru.hpp>
|
|
#include <machines/machinebase.h>
|
|
#include <memaccess.h>
|
|
|
|
#include <bitset>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
using namespace ata_interface;
|
|
|
|
AtaHardDisk::AtaHardDisk(std::string name) : AtaBaseDevice(name, DEVICE_TYPE_ATA) {
|
|
}
|
|
|
|
int AtaHardDisk::device_postinit() {
|
|
std::string hdd_config = GET_STR_PROP("hdd_config");
|
|
if (hdd_config.empty()) {
|
|
LOG_F(ERROR, "%s: hdd_config property is empty", this->name.c_str());
|
|
return -1;
|
|
}
|
|
|
|
std::string hdd_image_path = GET_STR_PROP("hdd_img");
|
|
if (hdd_image_path.empty())
|
|
return 0;
|
|
|
|
std::string bus_id;
|
|
uint32_t dev_num;
|
|
|
|
parse_device_path(hdd_config, bus_id, dev_num);
|
|
|
|
auto bus_obj = dynamic_cast<IdeChannel*>(gMachineObj->get_comp_by_name(bus_id));
|
|
bus_obj->register_device(dev_num, this);
|
|
|
|
this->insert_image(hdd_image_path);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void AtaHardDisk::insert_image(std::string filename) {
|
|
if (!this->hdd_img.open(filename)) {
|
|
ABORT_F("%s: could not open image file \"%s\"", this->name.c_str(),
|
|
filename.c_str());
|
|
}
|
|
|
|
this->img_size = this->hdd_img.size();
|
|
uint64_t sectors = this->hdd_img.size() / ATA_HD_SEC_SIZE;
|
|
this->total_sectors = (uint32_t)sectors;
|
|
if (sectors != this->total_sectors) {
|
|
ABORT_F("%s: image file \"%s\" is too big", this->name.c_str(),
|
|
filename.c_str());
|
|
}
|
|
this->calc_chs_params();
|
|
}
|
|
|
|
int AtaHardDisk::perform_command() {
|
|
this->r_status |= BSY;
|
|
this->r_error = 0;
|
|
|
|
switch (this->r_command) {
|
|
case NOP:
|
|
break;
|
|
case ATAPI_SOFT_RESET: // for ATA devices, is a no-op
|
|
this->r_status &= ~BSY;
|
|
break;
|
|
case RECALIBRATE: // is optional in ATA-3; disappeared in >= ATA-4
|
|
// OF 3.1.1 won't boot off the drive that reports error for this command
|
|
this->r_cylinder_hi = 0;
|
|
this->r_cylinder_lo = 0;
|
|
this->r_dev_head &= 0xF0;
|
|
this->r_sect_num = (this->r_dev_head & ATA_Dev_Head::LBA) ? 0 : 1;
|
|
this->r_status &= ~BSY;
|
|
this->update_intrq(1);
|
|
break;
|
|
case READ_MULTIPLE:
|
|
case READ_SECTOR:
|
|
case READ_SECTOR_NR: {
|
|
uint16_t sec_count = this->r_sect_count ? this->r_sect_count : 256;
|
|
int xfer_size = sec_count * ATA_HD_SEC_SIZE;
|
|
uint64_t offset = this->get_lba() * ATA_HD_SEC_SIZE;
|
|
uint32_t ints_size = ATA_HD_SEC_SIZE;
|
|
if (this->r_command == READ_MULTIPLE) {
|
|
if (this->sec_per_block == 0) {
|
|
LOG_F(ERROR, "%s: cannot do a READ_MULTIPLE with unset sec_per_block", this->name.c_str());
|
|
this->r_status |= ERR;
|
|
this->r_status &= ~BSY;
|
|
break;
|
|
}
|
|
ints_size *= this->sec_per_block;
|
|
}
|
|
hdd_img.read(buffer, offset, xfer_size);
|
|
this->data_ptr = (uint16_t *)this->buffer;
|
|
// those commands should generate IRQ for each sector
|
|
this->prepare_xfer(xfer_size, ints_size);
|
|
this->signal_data_ready();
|
|
}
|
|
break;
|
|
case WRITE_MULTIPLE:
|
|
case WRITE_SECTOR:
|
|
case WRITE_SECTOR_NR: {
|
|
uint16_t sec_count = this->r_sect_count ? this->r_sect_count : 256;
|
|
this->cur_fpos = this->get_lba() * ATA_HD_SEC_SIZE;
|
|
this->data_ptr = (uint16_t *)this->buffer;
|
|
this->cur_data_ptr = this->data_ptr;
|
|
uint32_t xfer_size = sec_count * ATA_HD_SEC_SIZE;
|
|
uint32_t ints_size = ATA_HD_SEC_SIZE;
|
|
if (this->r_command == WRITE_MULTIPLE) {
|
|
if (this->sec_per_block == 0) {
|
|
LOG_F(ERROR, "%s: cannot do a WRITE_MULTIPLE with unset sec_per_block", this->name.c_str());
|
|
this->r_status |= ERR;
|
|
this->r_status &= ~BSY;
|
|
break;
|
|
}
|
|
ints_size *= this->sec_per_block;
|
|
}
|
|
this->prepare_xfer(xfer_size, ints_size);
|
|
this->post_xfer_action = [this]() {
|
|
uint64_t write_len = (this->cur_data_ptr - this->data_ptr) * sizeof(this->data_ptr[0]);
|
|
this->hdd_img.write(this->data_ptr, this->cur_fpos, write_len);
|
|
this->cur_fpos += write_len;
|
|
};
|
|
this->r_status |= DRQ;
|
|
this->r_status &= ~BSY;
|
|
}
|
|
break;
|
|
case DIAGNOSTICS:
|
|
this->r_error = 1; // device 0 passed, device 1 passed or not present
|
|
this->device_set_signature();
|
|
this->r_status &= ~BSY;
|
|
this->update_intrq(1);
|
|
break;
|
|
case READ_VERIFY:
|
|
// verify sectors are readable, just no-op
|
|
this->r_status &= ~BSY;
|
|
this->update_intrq(1);
|
|
break;
|
|
case INIT_DEV_PARAM:
|
|
// update fictive disk geometry with parameters from host
|
|
this->sectors = this->r_sect_count;
|
|
this->heads = (this->r_dev_head & 0xF) + 1;
|
|
this->r_status &= ~BSY;
|
|
this->update_intrq(1);
|
|
break;
|
|
case SET_MULTIPLE_MODE: // this command is mandatory for ATA devices
|
|
if (!this->r_sect_count || this->r_sect_count > 128 ||
|
|
std::bitset<8>(this->r_sect_count).count() != 1) { // power of two?
|
|
LOG_F(ERROR, "%s: SET_MULTIPLE_MODE not suported, invalid r_sect_count (%d)", this->name.c_str(), this->r_sect_count);
|
|
this->multiple_enabled = false;
|
|
this->r_error |= ABRT;
|
|
this->r_status |= ERR;
|
|
} else {
|
|
LOG_F(INFO, "%s: SET_MULTIPLE_MODE, r_sect_count=%d", this->name.c_str(), this->r_sect_count);
|
|
this->sec_per_block = this->r_sect_count;
|
|
this->multiple_enabled = true;
|
|
}
|
|
this->r_status &= ~BSY;
|
|
this->update_intrq(1);
|
|
break;
|
|
case FLUSH_CACHE: // used by the XNU kernel driver
|
|
this->r_status &= ~(BSY | DRQ | ERR);
|
|
this->update_intrq(1);
|
|
break;
|
|
case IDENTIFY_DEVICE:
|
|
this->prepare_identify_info();
|
|
this->data_ptr = (uint16_t *)this->data_buf;
|
|
this->prepare_xfer(ATA_HD_SEC_SIZE, ATA_HD_SEC_SIZE);
|
|
this->signal_data_ready();
|
|
break;
|
|
case SET_FEATURES:
|
|
if (this->r_features == 3) {
|
|
switch(this->r_sect_count >> 3) {
|
|
case 0:
|
|
LOG_F(INFO, "%s: default transfer mode requested", this->name.c_str());
|
|
break;
|
|
case 1:
|
|
LOG_F(INFO, "%s: PIO transfer mode set to 0x%X", this->name.c_str(),
|
|
this->r_sect_count & 7);
|
|
break;
|
|
case 4:
|
|
LOG_F(INFO, "%s: Multiword DMA mode set to 0x%X", this->name.c_str(),
|
|
this->r_sect_count & 7);
|
|
break;
|
|
default:
|
|
LOG_F(ERROR, "%s: unsupported transfer mode 0x%X", this->name.c_str(),
|
|
this->r_sect_count);
|
|
this->r_error |= ATA_Error::ABRT;
|
|
this->r_status |= ATA_Status::ERR;
|
|
}
|
|
} else {
|
|
LOG_F(WARNING, "%s: unsupported SET_FEATURES subcommand code 0x%X",
|
|
this->name.c_str(), this->r_features);
|
|
}
|
|
this->r_status &= ~BSY;
|
|
this->update_intrq(1);
|
|
break;
|
|
case STANDBY_IMMEDIATE_E0:
|
|
LOG_F(INFO, "%s: STANDBY_IMMEDIATE_E0", this->name.c_str());
|
|
this->r_status &= ~BSY;
|
|
this->update_intrq(1);
|
|
break;
|
|
default:
|
|
LOG_F(ERROR, "%s: unknown ATA command 0x%x", this->name.c_str(), this->r_command);
|
|
this->r_status &= ~BSY;
|
|
this->r_status |= ERR;
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void AtaHardDisk::prepare_identify_info() {
|
|
uint16_t *buf_ptr = (uint16_t *)this->data_buf;
|
|
|
|
std::memset(this->data_buf, 0, sizeof(this->data_buf));
|
|
|
|
buf_ptr[ 0] = 0x0040; // ATA device, non-removable media, non-removable drive
|
|
buf_ptr[49] = 0x0200; // report LBA support
|
|
|
|
// Maximum number of logical sectors per data block that the device supports
|
|
// for READ_MULTIPLE/WRITE_MULTIPLE commands.
|
|
const int max_sec_per_block = 8;
|
|
if (max_sec_per_block > 1) {
|
|
buf_ptr[47] = 0x8000 | max_sec_per_block;
|
|
}
|
|
// If bit 8 of word 59 is set to one, then bits 7:0 indicate the number of
|
|
// logical sectors that shall be transferred per data block for
|
|
// READ_MULTIPLE/WRITE_MULTIPLE commands.
|
|
if (this->sec_per_block) {
|
|
buf_ptr[59] = 0x100 | this->sec_per_block;
|
|
}
|
|
|
|
buf_ptr[ 1] = this->cylinders;
|
|
buf_ptr[ 3] = this->heads;
|
|
buf_ptr[ 6] = this->sectors;
|
|
|
|
buf_ptr[57] = this->total_sectors & 0xFFFFU;
|
|
buf_ptr[58] = (this->total_sectors >> 16) & 0xFFFFU;
|
|
|
|
// report LBA capacity
|
|
WRITE_WORD_LE_A(&buf_ptr[60], (this->total_sectors & 0xFFFFU));
|
|
WRITE_WORD_LE_A(&buf_ptr[61], (this->total_sectors >> 16) & 0xFFFFU);
|
|
}
|
|
|
|
uint64_t AtaHardDisk::get_lba() {
|
|
if (this->r_dev_head & ATA_Dev_Head::LBA) {
|
|
return ((this->r_dev_head & 0xF) << 24) | (this->r_cylinder_hi << 16) |
|
|
(this->r_cylinder_lo << 8) | (this->r_sect_num);
|
|
} else { // translate old fashioned CHS addressing to LBA
|
|
uint16_t c = (this->r_cylinder_hi << 8) + this->r_cylinder_lo;
|
|
uint8_t h = this->r_dev_head & 0xF;
|
|
uint8_t s = this->r_sect_num;
|
|
|
|
if (!s) {
|
|
LOG_F(ERROR, "%s: zero sector number is not allowed!", this->name.c_str());
|
|
return -1ULL;
|
|
} else
|
|
return (this->heads * c + h) * this->sectors + s - 1;
|
|
}
|
|
}
|
|
|
|
void AtaHardDisk::calc_chs_params() {
|
|
unsigned num_blocks, heads, sectors, max_sectors, cylinders, max_cylinders;
|
|
|
|
LOG_F(INFO, "%s: total sectors %d", this->name.c_str(), this->total_sectors);
|
|
|
|
if (this->total_sectors >= REAL_CHS_LIMIT) {
|
|
heads = 16;
|
|
sectors = 255;
|
|
cylinders = 65535;
|
|
LOG_F(WARNING, "%s: exceeds max CHS translation",
|
|
this->name.c_str());
|
|
goto done;
|
|
}
|
|
|
|
// use PC BIOS limit to keep number of sectors small for smaller disks
|
|
if (this->total_sectors >= ATA_BIOS_LIMIT) {
|
|
max_sectors = 255;
|
|
max_cylinders = 65535;
|
|
} else {
|
|
max_sectors = 63;
|
|
max_cylinders = 16383;
|
|
}
|
|
|
|
num_blocks = this->total_sectors;
|
|
|
|
for (heads = 16; heads > 0; heads--)
|
|
for (sectors = max_sectors; sectors > 0; sectors--)
|
|
if (!(num_blocks % (heads * sectors)))
|
|
if (heads * sectors * max_cylinders >= num_blocks) {
|
|
cylinders = num_blocks / (heads * sectors);
|
|
goto done;
|
|
}
|
|
|
|
heads = 16;
|
|
sectors = (num_blocks + heads * max_cylinders - 1) / (heads * max_cylinders);
|
|
cylinders = (num_blocks + heads * sectors - 1) / (heads * sectors);
|
|
|
|
LOG_F(WARNING, "%s: could not find a suitable CHS translation; increased sectors to %d",
|
|
this->name.c_str(), heads * sectors * cylinders);
|
|
|
|
done:
|
|
this->heads = heads;
|
|
this->sectors = sectors;
|
|
this->cylinders = cylinders;
|
|
LOG_F(INFO, "%s: C=%d, H=%d, S=%d", this->name.c_str(), cylinders,
|
|
heads, sectors);
|
|
}
|
|
|
|
static const PropMap AtaHardDiskProperties = {
|
|
{"hdd_img", new StrProperty("")},
|
|
};
|
|
|
|
static const DeviceDescription AtaHardDiskDescriptor =
|
|
{AtaHardDisk::create, {}, AtaHardDiskProperties};
|
|
|
|
REGISTER_DEVICE(AtaHardDisk, AtaHardDiskDescriptor);
|