mirror of
https://github.com/Atmosphere-NX/Atmosphere.git
synced 2024-12-27 12:46:03 +00:00
471 lines
22 KiB
C++
471 lines
22 KiB
C++
/*
|
|
* Copyright (c) Atmosphère-NX
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms and conditions of the GNU General Public License,
|
|
* version 2, as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include <exosphere.hpp>
|
|
#include "../secmon_error.hpp"
|
|
#include "../secmon_map.hpp"
|
|
#include "../secmon_misc.hpp"
|
|
#include "../secmon_page_mapper.hpp"
|
|
#include "../secmon_user_power_management.hpp"
|
|
#include "secmon_smc_info.hpp"
|
|
#include "secmon_smc_power_management.hpp"
|
|
|
|
namespace ams::secmon::smc {
|
|
|
|
namespace {
|
|
|
|
struct KernelConfiguration {
|
|
/* Secure Monitor view. */
|
|
using Flags1 = util::BitPack32::Field< 0, 8>;
|
|
using Flags0 = util::BitPack32::Field< 8, 8>;
|
|
using PhysicalMemorySize = util::BitPack32::Field<16, 2>;
|
|
|
|
/* Kernel view, from libmesosphere. */
|
|
using DebugFillMemory = util::BitPack32::Field<0, 1, bool>;
|
|
using EnableUserExceptionHandlers = util::BitPack32::Field<DebugFillMemory::Next, 1, bool>;
|
|
using EnableUserPmuAccess = util::BitPack32::Field<EnableUserExceptionHandlers::Next, 1, bool>;
|
|
using IncreaseThreadResourceLimit = util::BitPack32::Field<EnableUserPmuAccess::Next, 1, bool>;
|
|
using DisableDynamicResourceLimits = util::BitPack32::Field<IncreaseThreadResourceLimit::Next, 1, bool>;
|
|
using Reserved5 = util::BitPack32::Field<DisableDynamicResourceLimits::Next, 3, u32>;
|
|
using UseSecureMonitorPanicCall = util::BitPack32::Field<Reserved5::Next, 1, bool>;
|
|
using Reserved9 = util::BitPack32::Field<UseSecureMonitorPanicCall::Next, 7, u32>;
|
|
using MemorySize = util::BitPack32::Field<Reserved9::Next, 2, u32>; /* smc::MemorySize = pkg1::MemorySize */
|
|
};
|
|
|
|
constexpr const pkg1::MemorySize DramIdToMemorySize[fuse::DramId_Count] = {
|
|
[fuse::DramId_IcosaSamsung4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IcosaHynix4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IcosaMicron4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaHynix1y4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IcosaSamsung6GB] = pkg1::MemorySize_6GB,
|
|
[fuse::DramId_HoagHynix1y4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_AulaHynix1y4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaX1X2Samsung4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaSansung4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaSamsung8GB] = pkg1::MemorySize_8GB,
|
|
[fuse::DramId_IowaHynix4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaMicron4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_HoagSamsung4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_HoagSamsung8GB] = pkg1::MemorySize_8GB,
|
|
[fuse::DramId_HoagHynix4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_HoagMicron4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaSamsung4GBY] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaSamsung1y4GBX] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaSamsung1y8GBX] = pkg1::MemorySize_8GB,
|
|
[fuse::DramId_HoagSamsung1y4GBX] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaSamsung1y4GBY] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaSamsung1y8GBY] = pkg1::MemorySize_8GB,
|
|
[fuse::DramId_AulaSamsung1y4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_HoagSamsung1y8GBX] = pkg1::MemorySize_8GB,
|
|
[fuse::DramId_AulaSamsung1y4GBX] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_IowaMicron1y4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_HoagMicron1y4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_AulaMicron1y4GB] = pkg1::MemorySize_4GB,
|
|
[fuse::DramId_AulaSamsung1y8GBX] = pkg1::MemorySize_8GB,
|
|
};
|
|
|
|
constexpr const pkg1::MemoryMode MemoryModes[] = {
|
|
pkg1::MemoryMode_Auto,
|
|
|
|
pkg1::MemoryMode_4GB,
|
|
pkg1::MemoryMode_4GBAppletDev,
|
|
pkg1::MemoryMode_4GBSystemDev,
|
|
|
|
pkg1::MemoryMode_6GB,
|
|
pkg1::MemoryMode_6GBAppletDev,
|
|
|
|
pkg1::MemoryMode_8GB,
|
|
};
|
|
|
|
constexpr bool IsValidMemoryMode(pkg1::MemoryMode mode) {
|
|
for (const auto known_mode : MemoryModes) {
|
|
if (mode == known_mode) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
pkg1::MemoryMode SanitizeMemoryMode(pkg1::MemoryMode mode) {
|
|
if (IsValidMemoryMode(mode)) {
|
|
return mode;
|
|
}
|
|
return pkg1::MemoryMode_Auto;
|
|
}
|
|
|
|
pkg1::MemorySize GetAvailableMemorySize(pkg1::MemorySize size) {
|
|
return std::min(GetPhysicalMemorySize(), size);
|
|
}
|
|
|
|
pkg1::MemoryMode GetMemoryMode(pkg1::MemoryMode mode) {
|
|
/* Sanitize the mode. */
|
|
mode = SanitizeMemoryMode(mode);
|
|
|
|
/* If the mode is auto, construct the memory mode. */
|
|
if (mode == pkg1::MemoryMode_Auto) {
|
|
return pkg1::MakeMemoryMode(GetPhysicalMemorySize(), pkg1::MemoryArrange_Normal);
|
|
} else {
|
|
const auto mode_size = GetMemorySize(mode);
|
|
const auto mode_arrange = GetMemoryArrange(mode);
|
|
const auto size = GetAvailableMemorySize(mode_size);
|
|
const auto arrange = (size == mode_size) ? mode_arrange : pkg1::MemoryArrange_Normal;
|
|
return pkg1::MakeMemoryMode(size, arrange);
|
|
}
|
|
}
|
|
|
|
u32 GetMemoryMode() {
|
|
/* Unless development function is enabled, we're 4 GB. */
|
|
u32 memory_mode = pkg1::MemoryMode_4GB;
|
|
|
|
if (const auto &bcd = GetBootConfig().data; bcd.IsDevelopmentFunctionEnabled()) {
|
|
memory_mode = GetMemoryMode(bcd.GetMemoryMode());
|
|
}
|
|
|
|
return memory_mode;
|
|
}
|
|
|
|
u32 GetKernelConfiguration() {
|
|
pkg1::MemorySize memory_size = pkg1::MemorySize_4GB;
|
|
util::BitPack32 value = {};
|
|
|
|
if (const auto &bcd = GetBootConfig().data; bcd.IsDevelopmentFunctionEnabled()) {
|
|
memory_size = GetMemorySize(GetMemoryMode(bcd.GetMemoryMode()));
|
|
|
|
value.Set<KernelConfiguration::Flags1>(bcd.GetKernelFlags1());
|
|
value.Set<KernelConfiguration::Flags0>(bcd.GetKernelFlags0());
|
|
}
|
|
|
|
value.Set<KernelConfiguration::PhysicalMemorySize>(memory_size);
|
|
|
|
/* Exosphere extensions. */
|
|
const auto &sc = GetSecmonConfiguration();
|
|
|
|
if (!sc.DisableUserModeExceptionHandlers()) {
|
|
value.Set<KernelConfiguration::EnableUserExceptionHandlers>(true);
|
|
}
|
|
|
|
if (sc.EnableUserModePerformanceCounterAccess()) {
|
|
value.Set<KernelConfiguration::EnableUserPmuAccess>(true);
|
|
}
|
|
|
|
return value.value;
|
|
}
|
|
|
|
constinit u64 g_payload_address = 0;
|
|
constinit bool g_set_true_target_firmware = false;
|
|
|
|
SmcResult GetConfig(SmcArguments &args, bool kern) {
|
|
switch (static_cast<ConfigItem>(args.r[1])) {
|
|
case ConfigItem::DisableProgramVerification:
|
|
args.r[1] = GetBootConfig().signed_data.IsProgramVerificationDisabled();
|
|
break;
|
|
case ConfigItem::DramId:
|
|
args.r[1] = fuse::GetDramId();
|
|
break;
|
|
case ConfigItem::SecurityEngineInterruptNumber:
|
|
args.r[1] = SecurityEngineUserInterruptId;
|
|
break;
|
|
case ConfigItem::FuseVersion:
|
|
args.r[1] = fuse::GetExpectedFuseVersion(GetTargetFirmware());
|
|
break;
|
|
case ConfigItem::HardwareType:
|
|
args.r[1] = fuse::GetHardwareType();
|
|
break;
|
|
case ConfigItem::HardwareState:
|
|
args.r[1] = fuse::GetHardwareState();
|
|
break;
|
|
case ConfigItem::IsRecoveryBoot:
|
|
args.r[1] = IsRecoveryBoot();
|
|
break;
|
|
case ConfigItem::DeviceId:
|
|
args.r[1] = fuse::GetDeviceId();
|
|
break;
|
|
case ConfigItem::BootReason:
|
|
{
|
|
/* This was removed in firmware 4.0.0. */
|
|
if (GetTargetFirmware() >= TargetFirmware_4_0_0) {
|
|
return SmcResult::InvalidArgument;
|
|
}
|
|
|
|
args.r[1] = GetDeprecatedBootReason();
|
|
}
|
|
break;
|
|
case ConfigItem::MemoryMode:
|
|
args.r[1] = GetMemoryMode();
|
|
break;
|
|
case ConfigItem::IsDevelopmentFunctionEnabled:
|
|
args.r[1] = GetSecmonConfiguration().IsDevelopmentFunctionEnabled(kern) || GetBootConfig().data.IsDevelopmentFunctionEnabled();
|
|
break;
|
|
case ConfigItem::KernelConfiguration:
|
|
args.r[1] = GetKernelConfiguration();
|
|
break;
|
|
case ConfigItem::IsChargerHiZModeEnabled:
|
|
args.r[1] = IsChargerHiZModeEnabled();
|
|
break;
|
|
case ConfigItem::RetailInteractiveDisplayState:
|
|
args.r[1] = fuse::GetRetailInteractiveDisplayState();
|
|
break;
|
|
case ConfigItem::RegulatorType:
|
|
args.r[1] = fuse::GetRegulator();
|
|
break;
|
|
case ConfigItem::DeviceUniqueKeyGeneration:
|
|
args.r[1] = fuse::GetDeviceUniqueKeyGeneration();
|
|
break;
|
|
case ConfigItem::Package2Hash:
|
|
{
|
|
/* Only allow getting the package2 hash in recovery boot. */
|
|
if (!IsRecoveryBoot()) {
|
|
return SmcResult::InvalidArgument;
|
|
}
|
|
|
|
/* Get the hash. */
|
|
se::Sha256Hash tmp_hash;
|
|
GetPackage2Hash(std::addressof(tmp_hash));
|
|
|
|
/* Copy it out. */
|
|
static_assert(sizeof(args) - sizeof(args.r[0]) >= sizeof(tmp_hash));
|
|
std::memcpy(std::addressof(args.r[1]), std::addressof(tmp_hash), sizeof(tmp_hash));
|
|
}
|
|
break;
|
|
case ConfigItem::ExosphereApiVersion:
|
|
/* Get information about the current exosphere version. */
|
|
if (kern || g_set_true_target_firmware) {
|
|
args.r[1] = (static_cast<u64>(ATMOSPHERE_RELEASE_VERSION_MAJOR & 0xFF) << 56) |
|
|
(static_cast<u64>(ATMOSPHERE_RELEASE_VERSION_MINOR & 0xFF) << 48) |
|
|
(static_cast<u64>(ATMOSPHERE_RELEASE_VERSION_MICRO & 0xFF) << 40) |
|
|
(static_cast<u64>(GetKeyGeneration()) << 32) |
|
|
(static_cast<u64>(GetTargetFirmware()) << 0);
|
|
} else {
|
|
return SmcResult::NotInitialized;
|
|
}
|
|
break;
|
|
case ConfigItem::ExosphereNeedsReboot:
|
|
/* We are executing, so we aren't in the process of rebooting. */
|
|
args.r[1] = 0;
|
|
break;
|
|
case ConfigItem::ExosphereNeedsShutdown:
|
|
/* We are executing, so we aren't in the process of shutting down. */
|
|
args.r[1] = 0;
|
|
break;
|
|
case ConfigItem::ExosphereGitCommitHash:
|
|
/* Get information about the current exosphere git commit hash. */
|
|
args.r[1] = ATMOSPHERE_GIT_HASH;
|
|
break;
|
|
case ConfigItem::ExosphereHasRcmBugPatch:
|
|
/* Get information about whether this unit has the RCM bug patched. */
|
|
args.r[1] = fuse::HasRcmVulnerabilityPatch();
|
|
break;
|
|
case ConfigItem::ExosphereBlankProdInfo:
|
|
/* Get whether this unit should simulate a "blanked" PRODINFO. */
|
|
args.r[1] = GetSecmonConfiguration().ShouldUseBlankCalibrationBinary();
|
|
break;
|
|
case ConfigItem::ExosphereAllowCalWrites:
|
|
/* Get whether this unit should allow writing to the calibration partition. */
|
|
args.r[1] = (GetEmummcConfiguration().IsEmummcActive() || GetSecmonConfiguration().AllowWritingToCalibrationBinarySysmmc());
|
|
break;
|
|
case ConfigItem::ExosphereEmummcType:
|
|
/* Get what kind of emummc this unit has active. */
|
|
/* NOTE: This may return values other than 1 in the future. */
|
|
args.r[1] = (GetEmummcConfiguration().IsEmummcActive() ? 1 : 0);
|
|
break;
|
|
case ConfigItem::ExospherePayloadAddress:
|
|
/* Gets the physical address of the reboot payload buffer, if one exists. */
|
|
if (g_payload_address != 0) {
|
|
args.r[1] = g_payload_address;
|
|
} else {
|
|
return SmcResult::NotInitialized;
|
|
}
|
|
break;
|
|
case ConfigItem::ExosphereLogConfiguration:
|
|
/* Get the log configuration. */
|
|
args.r[1] = (static_cast<u64>(static_cast<u8>(secmon::GetLogPort())) << 32) | static_cast<u64>(secmon::GetLogBaudRate());
|
|
break;
|
|
case ConfigItem::ExosphereForceEnableUsb30:
|
|
/* Get whether usb 3.0 should be force-enabled. */
|
|
args.r[1] = GetSecmonConfiguration().IsUsb30ForceEnabled();
|
|
break;
|
|
case ConfigItem::ExosphereSupportedHosVersion:
|
|
/* Get information about the supported hos version. */
|
|
args.r[1] = (static_cast<u64>(ATMOSPHERE_SUPPORTED_HOS_VERSION_MAJOR & 0xFF) << 24) |
|
|
(static_cast<u64>(ATMOSPHERE_SUPPORTED_HOS_VERSION_MINOR & 0xFF) << 16) |
|
|
(static_cast<u64>(ATMOSPHERE_SUPPORTED_HOS_VERSION_MICRO & 0xFF) << 8);
|
|
break;
|
|
case ConfigItem::ExosphereApproximateApiVersion:
|
|
/* Get information about the current exosphere version. */
|
|
if (!g_set_true_target_firmware) {
|
|
args.r[1] = (static_cast<u64>(ATMOSPHERE_RELEASE_VERSION_MAJOR & 0xFF) << 56) |
|
|
(static_cast<u64>(ATMOSPHERE_RELEASE_VERSION_MINOR & 0xFF) << 48) |
|
|
(static_cast<u64>(ATMOSPHERE_RELEASE_VERSION_MICRO & 0xFF) << 40) |
|
|
(static_cast<u64>(GetKeyGeneration()) << 32) |
|
|
(static_cast<u64>(GetTargetFirmware()) << 0);
|
|
} else {
|
|
return SmcResult::Busy;
|
|
}
|
|
break;
|
|
default:
|
|
return SmcResult::InvalidArgument;
|
|
}
|
|
|
|
return SmcResult::Success;
|
|
}
|
|
|
|
SmcResult SetConfig(SmcArguments &args) {
|
|
const auto soc_type = GetSocType();
|
|
|
|
switch (static_cast<ConfigItem>(args.r[1])) {
|
|
case ConfigItem::IsChargerHiZModeEnabled:
|
|
/* Configure the HiZ mode. */
|
|
SetChargerHiZModeEnabled(static_cast<bool>(args.r[3]));
|
|
break;
|
|
case ConfigItem::ExosphereApiVersion:
|
|
if (!g_set_true_target_firmware) {
|
|
::ams::secmon::impl::SetTargetFirmware(static_cast<ams::TargetFirmware>(args.r[3] & 0xFFFFFFFF));
|
|
g_set_true_target_firmware = true;
|
|
} else {
|
|
return SmcResult::Busy;
|
|
}
|
|
break;
|
|
case ConfigItem::ExosphereNeedsReboot:
|
|
if (soc_type == fuse::SocType_Erista) {
|
|
switch (static_cast<UserRebootType>(args.r[3])) {
|
|
case UserRebootType_None:
|
|
break;
|
|
case UserRebootType_ToRcm:
|
|
PerformUserRebootToRcm();
|
|
break;
|
|
case UserRebootType_ToPayload:
|
|
PerformUserRebootToPayload();
|
|
break;
|
|
case UserRebootType_ToFatalError:
|
|
PerformUserRebootToFatalError();
|
|
break;
|
|
default:
|
|
return SmcResult::InvalidArgument;
|
|
}
|
|
} else /* if (soc_type == fuse::SocType_Mariko) */ {
|
|
switch (static_cast<UserRebootType>(args.r[3])) {
|
|
case UserRebootType_ToFatalError:
|
|
PerformUserRebootToFatalError();
|
|
break;
|
|
default:
|
|
return SmcResult::InvalidArgument;
|
|
}
|
|
}
|
|
break;
|
|
case ConfigItem::ExosphereNeedsShutdown:
|
|
if (soc_type == fuse::SocType_Erista) {
|
|
if (args.r[3] != 0) {
|
|
PerformUserShutDown();
|
|
}
|
|
} else /* if (soc_type == fuse::SocType_Mariko) */ {
|
|
return SmcResult::NotSupported;
|
|
}
|
|
break;
|
|
case ConfigItem::ExospherePayloadAddress:
|
|
if (g_payload_address == 0) {
|
|
if (secmon::IsPhysicalMemoryAddress(args.r[2])) {
|
|
g_payload_address = args.r[2];
|
|
} else {
|
|
return SmcResult::InvalidArgument;
|
|
}
|
|
} else {
|
|
return SmcResult::Busy;
|
|
}
|
|
break;
|
|
default:
|
|
return SmcResult::InvalidArgument;
|
|
}
|
|
|
|
return SmcResult::Success;
|
|
}
|
|
|
|
}
|
|
|
|
SmcResult SmcGetConfigUser(SmcArguments &args) {
|
|
return GetConfig(args, false);
|
|
}
|
|
|
|
SmcResult SmcGetConfigKern(SmcArguments &args) {
|
|
return GetConfig(args, true);
|
|
}
|
|
|
|
SmcResult SmcSetConfig(SmcArguments &args) {
|
|
return SetConfig(args);
|
|
}
|
|
|
|
/* This is an atmosphere extension smc. */
|
|
SmcResult SmcGetEmummcConfig(SmcArguments &args) {
|
|
/* Decode arguments. */
|
|
const auto mmc = static_cast<EmummcMmc>(args.r[1]);
|
|
const uintptr_t user_address = args.r[2];
|
|
const uintptr_t user_offset = user_address % 4_KB;
|
|
|
|
/* Validate arguments. */
|
|
/* NOTE: In the future, configuration for non-NAND storage may be implemented. */
|
|
SMC_R_UNLESS(mmc == EmummcMmc_Nand, NotSupported);
|
|
SMC_R_UNLESS(user_offset + 2 * sizeof(EmummcFilePath) <= 4_KB, InvalidArgument);
|
|
|
|
/* Get the emummc config. */
|
|
const auto &cfg = GetEmummcConfiguration();
|
|
static_assert(sizeof(cfg.file_cfg) == sizeof(EmummcFilePath));
|
|
static_assert(sizeof(cfg.emu_dir_path) == sizeof(EmummcFilePath));
|
|
|
|
/* Clear the output. */
|
|
constexpr size_t InlineOutputSize = sizeof(args) - sizeof(args.r[0]);
|
|
u8 * const inline_output = static_cast<u8 *>(static_cast<void *>(std::addressof(args.r[1])));
|
|
std::memset(inline_output, 0, InlineOutputSize);
|
|
|
|
/* Copy out the configuration. */
|
|
{
|
|
/* Map the user output page. */
|
|
AtmosphereUserPageMapper mapper(user_address);
|
|
SMC_R_UNLESS(mapper.Map(), InvalidArgument);
|
|
|
|
/* Copy the base configuration. */
|
|
static_assert(sizeof(cfg.base_cfg) <= InlineOutputSize);
|
|
std::memcpy(inline_output, std::addressof(cfg.base_cfg), sizeof(cfg.base_cfg));
|
|
|
|
/* Copy out type-specific data. */
|
|
switch (cfg.base_cfg.type) {
|
|
case EmummcType_None:
|
|
/* No additional configuration needs to be copied. */
|
|
break;
|
|
case EmummcType_Partition:
|
|
/* Copy the partition config. */
|
|
static_assert(sizeof(cfg.base_cfg) + sizeof(cfg.partition_cfg) <= InlineOutputSize);
|
|
std::memcpy(inline_output + sizeof(cfg.base_cfg), std::addressof(cfg.partition_cfg), sizeof(cfg.partition_cfg));
|
|
break;
|
|
case EmummcType_File:
|
|
/* Copy the file config. */
|
|
SMC_R_UNLESS(mapper.CopyToUser(user_address, std::addressof(cfg.file_cfg), sizeof(cfg.file_cfg)), InvalidArgument);
|
|
break;
|
|
AMS_UNREACHABLE_DEFAULT_CASE();
|
|
}
|
|
|
|
/* Copy the redirection directory path to the user page. */
|
|
SMC_R_UNLESS(mapper.CopyToUser(user_address + sizeof(EmummcFilePath), std::addressof(cfg.emu_dir_path), sizeof(cfg.emu_dir_path)), InvalidArgument);
|
|
}
|
|
|
|
return SmcResult::Success;
|
|
}
|
|
|
|
/* For exosphere's usage. */
|
|
pkg1::MemorySize GetPhysicalMemorySize() {
|
|
const auto dram_id = fuse::GetDramId();
|
|
AMS_ABORT_UNLESS(dram_id < fuse::DramId_Count);
|
|
return DramIdToMemorySize[dram_id];
|
|
}
|
|
|
|
}
|