| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "aml-cpu.h" |
| |
| #include <fuchsia/hardware/platform/device/cpp/banjo.h> |
| #include <fuchsia/hardware/thermal/cpp/banjo.h> |
| #include <fuchsia/hardware/thermal/llcpp/fidl.h> |
| #include <lib/device-protocol/pdev.h> |
| #include <lib/fake_ddk/fake_ddk.h> |
| #include <lib/fake_ddk/fidl-helper.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <vector> |
| |
| #include <ddktl/device.h> |
| #include <ddktl/fidl.h> |
| #include <fake-mmio-reg/fake-mmio-reg.h> |
| #include <fbl/array.h> |
| #include <sdk/lib/inspect/testing/cpp/zxtest/inspect.h> |
| #include <soc/aml-common/aml-cpu-metadata.h> |
| #include <zxtest/zxtest.h> |
| |
| #include "src/devices/bus/testing/fake-pdev/fake-pdev.h" |
| |
| namespace amlogic_cpu { |
| |
| // This subclass of Bind is only used to test the binding of AmlCpu. DeviceAdd is overridden |
| // to test expectation on devices that are added. |
| class Bind : public fake_ddk::Bind { |
| public: |
| Bind() : fake_ddk::Bind() {} |
| |
| zx_status_t DeviceAdd(zx_driver_t* drv, zx_device_t* parent, device_add_args_t* args, |
| zx_device_t** out) override { |
| if (parent != fake_ddk::kFakeParent || !args || args->proto_id != ZX_PROTOCOL_CPU_CTRL || |
| args->ctx == nullptr) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| devices_.push_back(std::unique_ptr<AmlCpu>(static_cast<AmlCpu*>(args->ctx))); |
| return ZX_OK; |
| } |
| |
| const std::vector<std::unique_ptr<AmlCpu>>& get_devices() { return devices_; } |
| |
| size_t num_devices_added() const { return devices_.size(); } |
| |
| private: |
| // The bind function intentionally leaks created devices, so they must be owned here. |
| std::vector<std::unique_ptr<AmlCpu>> devices_; |
| }; |
| |
| // Fake MMIO that exposes CPU version. |
| class FakeMmio { |
| public: |
| FakeMmio() { |
| regs_ = std::make_unique<ddk_fake::FakeMmioReg[]>(kRegCount); |
| mmio_ = std::make_unique<ddk_fake::FakeMmioRegRegion>(regs_.get(), sizeof(uint32_t), kRegCount); |
| (*mmio_)[kCpuVersionOffset].SetReadCallback([]() { return kCpuVersion; }); |
| } |
| |
| fake_pdev::FakePDev::MmioInfo mmio_info() { return {.offset = reinterpret_cast<size_t>(this)}; } |
| |
| ddk::MmioBuffer mmio() { return ddk::MmioBuffer(mmio_->GetMmioBuffer()); } |
| |
| private: |
| static constexpr size_t kCpuVersionOffset = 0x220; |
| static constexpr size_t kRegCount = kCpuVersionOffset / sizeof(uint32_t) + 1; |
| |
| // Note: FakeMmioReg's read callback returns a uint64_t, which is then cast to uint32_t when |
| // AmlCpu calls FakeMmioRegRegion::Read32. |
| constexpr static uint64_t kCpuVersion = 43; |
| |
| std::unique_ptr<ddk_fake::FakeMmioReg[]> regs_; |
| std::unique_ptr<ddk_fake::FakeMmioRegRegion> mmio_; |
| }; |
| |
| using CpuCtrlSyncClient = fidl::WireSyncClient<fuchsia_cpuctrl::Device>; |
| using ThermalSyncClient = fidl::WireSyncClient<fuchsia_thermal::Device>; |
| using fuchsia_device::wire::MAX_DEVICE_PERFORMANCE_STATES; |
| |
| constexpr size_t kBigClusterIdx = |
| static_cast<size_t>(fuchsia_thermal::wire::PowerDomain::BIG_CLUSTER_POWER_DOMAIN); |
| constexpr size_t kLittleClusterIdx = |
| static_cast<size_t>(fuchsia_thermal::wire::PowerDomain::LITTLE_CLUSTER_POWER_DOMAIN); |
| |
| constexpr uint32_t kBigClusterCoreCount = 4; |
| constexpr uint32_t kLittleClusterCoreCount = 2; |
| |
| constexpr legacy_cluster_size_t kClusterSizeMetadata[] = { |
| { |
| .pd_id = kBigClusterIdx, |
| .core_count = kBigClusterCoreCount, |
| }, |
| { |
| .pd_id = kLittleClusterIdx, |
| .core_count = kLittleClusterCoreCount, |
| }, |
| }; |
| |
| constexpr size_t PowerDomainToIndex(fuchsia_thermal::wire::PowerDomain pd) { |
| switch (pd) { |
| case fuchsia_thermal::wire::PowerDomain::LITTLE_CLUSTER_POWER_DOMAIN: |
| return kLittleClusterIdx; |
| case fuchsia_thermal::wire::PowerDomain::BIG_CLUSTER_POWER_DOMAIN: |
| return kBigClusterIdx; |
| } |
| __UNREACHABLE; |
| } |
| |
| const fuchsia_thermal::wire::OperatingPoint kFakeOperatingPoints = []() { |
| fuchsia_thermal::wire::OperatingPoint result; |
| |
| result.count = 3; |
| result.latency = 0; |
| result.opp[0].volt_uv = 1; |
| result.opp[0].freq_hz = 100; |
| result.opp[1].volt_uv = 2; |
| result.opp[1].freq_hz = 200; |
| result.opp[2].volt_uv = 3; |
| result.opp[2].freq_hz = 300; |
| |
| return result; |
| }(); |
| |
| const fuchsia_thermal::wire::ThermalDeviceInfo kDefaultDeviceInfo = []() { |
| fuchsia_thermal::wire::ThermalDeviceInfo result; |
| |
| result.active_cooling = false; |
| result.passive_cooling = false; |
| result.gpu_throttling = false; |
| result.num_trip_points = 0; |
| result.big_little = false; |
| result.critical_temp_celsius = 0; |
| |
| result.opps[kLittleClusterIdx].count = 0; |
| result.opps[kBigClusterIdx] = kFakeOperatingPoints; |
| |
| return result; |
| }(); |
| |
| class FakeAmlThermal : fidl::WireInterface<fuchsia_thermal::Device> { |
| public: |
| FakeAmlThermal() : active_operating_point_(0), device_info_(kDefaultDeviceInfo) {} |
| ~FakeAmlThermal() {} |
| |
| // Manage the Fake FIDL Message Loop |
| zx_status_t Init(std::optional<zx::channel> remote); |
| static zx_status_t MessageOp(void* ctx, fidl_incoming_msg_t* msg, fidl_txn_t* txn); |
| zx::channel& GetMessengerChannel() { return messenger_.local(); } |
| |
| // Accessor |
| uint16_t ActiveOperatingPoint() const { return active_operating_point_; } |
| |
| zx_status_t DdkMessage(fidl_incoming_msg_t* msg, fidl_txn_t* txn); |
| void DdkRelease() {} |
| |
| void set_device_info(const fuchsia_thermal::wire::ThermalDeviceInfo& device_info) { |
| device_info_ = device_info; |
| } |
| |
| private: |
| // Implement Thermal FIDL Protocol. |
| void GetInfo(GetInfoCompleter::Sync& completer); |
| void GetDeviceInfo(GetDeviceInfoCompleter::Sync& completer); |
| void GetDvfsInfo(fuchsia_thermal::wire::PowerDomain pd, GetDvfsInfoCompleter::Sync& completer); |
| void GetTemperatureCelsius(GetTemperatureCelsiusCompleter::Sync& completer); |
| void GetStateChangeEvent(GetStateChangeEventCompleter::Sync& completer); |
| void GetStateChangePort(GetStateChangePortCompleter::Sync& completer); |
| void SetTripCelsius(uint32_t id, float temp, SetTripCelsiusCompleter::Sync& completer); |
| void GetDvfsOperatingPoint(fuchsia_thermal::wire::PowerDomain pd, |
| GetDvfsOperatingPointCompleter::Sync& completer); |
| void SetDvfsOperatingPoint(uint16_t op_idx, fuchsia_thermal::wire::PowerDomain pd, |
| SetDvfsOperatingPointCompleter::Sync& completer); |
| void GetFanLevel(GetFanLevelCompleter::Sync& completer); |
| void SetFanLevel(uint32_t fan_level, SetFanLevelCompleter::Sync& completer); |
| |
| uint16_t active_operating_point_; |
| fake_ddk::FidlMessenger messenger_; |
| fuchsia_thermal::wire::ThermalDeviceInfo device_info_; |
| }; |
| |
| zx_status_t FakeAmlThermal::MessageOp(void* ctx, fidl_incoming_msg_t* msg, fidl_txn_t* txn) { |
| return static_cast<FakeAmlThermal*>(ctx)->DdkMessage(msg, txn); |
| } |
| |
| zx_status_t FakeAmlThermal::Init(std::optional<zx::channel> remote) { |
| return messenger_.SetMessageOp(this, FakeAmlThermal::MessageOp, std::move(remote)); |
| } |
| |
| zx_status_t FakeAmlThermal::DdkMessage(fidl_incoming_msg_t* msg, fidl_txn_t* txn) { |
| DdkTransaction transaction(txn); |
| fidl::WireDispatch<fuchsia_thermal::Device>(this, msg, &transaction); |
| return transaction.Status(); |
| } |
| |
| void FakeAmlThermal::GetInfo(GetInfoCompleter::Sync& completer) { |
| fuchsia_thermal::wire::ThermalInfo result; |
| |
| result.state = 0; |
| result.passive_temp_celsius = 0; |
| result.critical_temp_celsius = 0; |
| result.max_trip_count = 0; |
| |
| completer.Reply(ZX_OK, |
| fidl::ObjectView<fuchsia_thermal::wire::ThermalInfo>::FromExternal(&result)); |
| } |
| |
| void FakeAmlThermal::GetDeviceInfo(GetDeviceInfoCompleter::Sync& completer) { |
| fuchsia_thermal::wire::ThermalDeviceInfo result = device_info_; |
| completer.Reply( |
| ZX_OK, fidl::ObjectView<fuchsia_thermal::wire::ThermalDeviceInfo>::FromExternal(&result)); |
| } |
| |
| void FakeAmlThermal::GetDvfsInfo(fuchsia_thermal::wire::PowerDomain pd, |
| GetDvfsInfoCompleter::Sync& completer) { |
| fuchsia_thermal::wire::ThermalDeviceInfo device_info = device_info_; |
| fuchsia_thermal::wire::OperatingPoint result = device_info.opps[PowerDomainToIndex(pd)]; |
| completer.Reply(ZX_OK, |
| fidl::ObjectView<fuchsia_thermal::wire::OperatingPoint>::FromExternal(&result)); |
| } |
| |
| void FakeAmlThermal::GetTemperatureCelsius(GetTemperatureCelsiusCompleter::Sync& completer) { |
| completer.Reply(ZX_OK, 0.0); |
| } |
| |
| void FakeAmlThermal::GetStateChangeEvent(GetStateChangeEventCompleter::Sync& completer) { |
| zx::event invalid; |
| completer.Reply(ZX_ERR_NOT_SUPPORTED, std::move(invalid)); |
| } |
| |
| void FakeAmlThermal::GetStateChangePort(GetStateChangePortCompleter::Sync& completer) { |
| zx::port invalid; |
| completer.Reply(ZX_ERR_NOT_SUPPORTED, std::move(invalid)); |
| } |
| |
| void FakeAmlThermal::SetTripCelsius(uint32_t id, float temp, |
| SetTripCelsiusCompleter::Sync& completer) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| void FakeAmlThermal::GetDvfsOperatingPoint(fuchsia_thermal::wire::PowerDomain pd, |
| GetDvfsOperatingPointCompleter::Sync& completer) { |
| if (pd == fuchsia_thermal::wire::PowerDomain::LITTLE_CLUSTER_POWER_DOMAIN) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED, 0); |
| return; |
| } |
| |
| completer.Reply(ZX_OK, active_operating_point_); |
| } |
| |
| void FakeAmlThermal::SetDvfsOperatingPoint(uint16_t idx, fuchsia_thermal::wire::PowerDomain pd, |
| SetDvfsOperatingPointCompleter::Sync& completer) { |
| if (pd == fuchsia_thermal::wire::PowerDomain::LITTLE_CLUSTER_POWER_DOMAIN) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED); |
| return; |
| } |
| |
| active_operating_point_ = idx; |
| completer.Reply(ZX_OK); |
| } |
| |
| void FakeAmlThermal::GetFanLevel(GetFanLevelCompleter::Sync& completer) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED, 0); |
| } |
| |
| void FakeAmlThermal::SetFanLevel(uint32_t fan_level, SetFanLevelCompleter::Sync& completer) { |
| completer.Reply(ZX_ERR_OUT_OF_RANGE); |
| } |
| |
| // Fake device that exposes the thermal banjo protocol. Upon calling Connect, a new instance of |
| // FakeAmlThermal is created to serve a client, at which point any previous FakeThermalAml |
| // instance is destroyed. |
| class FakeThermalDevice : public ddk::ThermalProtocol<FakeThermalDevice, ddk::base_protocol> { |
| public: |
| FakeThermalDevice() |
| : proto_({&thermal_protocol_ops_, this}), device_info_(kDefaultDeviceInfo), fidl_service_() {} |
| |
| zx_status_t ThermalConnect(zx::channel chan) { |
| fidl_service_ = std::make_unique<FakeAmlThermal>(); |
| fidl_service_->set_device_info(device_info_); |
| return fidl_service_->Init({std::move(chan)}); |
| } |
| |
| const thermal_protocol_t* proto() const { return &proto_; } |
| |
| void set_device_info(const fuchsia_thermal::wire::ThermalDeviceInfo& device_info) { |
| device_info_ = device_info; |
| } |
| |
| private: |
| thermal_protocol_t proto_; |
| fuchsia_thermal::wire::ThermalDeviceInfo device_info_; |
| std::unique_ptr<FakeAmlThermal> fidl_service_; |
| }; |
| |
| // Fixture that supports tests of AmlCpu::Create. |
| class AmlCpuBindingTest : public zxtest::Test { |
| public: |
| AmlCpuBindingTest() { |
| pdev_.set_mmio(0, mmio_.mmio_info()); |
| |
| static constexpr size_t kNumBindFragments = 2; |
| |
| fbl::Array<fake_ddk::FragmentEntry> fragments(new fake_ddk::FragmentEntry[kNumBindFragments], |
| kNumBindFragments); |
| fragments[0] = pdev_.fragment(); |
| fragments[1].name = "thermal"; |
| fragments[1].protocols.emplace_back(fake_ddk::ProtocolEntry{ |
| ZX_PROTOCOL_THERMAL, |
| *reinterpret_cast<const fake_ddk::Protocol*>(thermal_device_.proto())}); |
| ddk_.SetFragments(std::move(fragments)); |
| |
| ddk_.SetMetadata(DEVICE_METADATA_CLUSTER_SIZE_LEGACY, &kClusterSizeMetadata, |
| sizeof(kClusterSizeMetadata)); |
| } |
| |
| zx_device_t* parent() { return fake_ddk::FakeParent(); } |
| |
| protected: |
| Bind ddk_; |
| fake_pdev::FakePDev pdev_; |
| FakeMmio mmio_; |
| FakeThermalDevice thermal_device_; |
| }; |
| |
| TEST_F(AmlCpuBindingTest, OneDomain) { |
| ASSERT_OK(AmlCpu::Create(nullptr, parent())); |
| ASSERT_EQ(ddk_.num_devices_added(), 1); |
| } |
| |
| TEST_F(AmlCpuBindingTest, TwoDomains) { |
| // Set up device info that defines two power domains. |
| thermal_device_.set_device_info([]() { |
| fuchsia_thermal::wire::ThermalDeviceInfo result; |
| |
| result.active_cooling = false; |
| result.passive_cooling = false; |
| result.gpu_throttling = false; |
| result.num_trip_points = 0; |
| result.big_little = true; |
| result.critical_temp_celsius = 0; |
| |
| result.opps[kLittleClusterIdx] = kFakeOperatingPoints; |
| result.opps[kBigClusterIdx] = kFakeOperatingPoints; |
| |
| return result; |
| }()); |
| |
| ASSERT_OK(AmlCpu::Create(nullptr, parent())); |
| ASSERT_EQ(ddk_.num_devices_added(), 2); |
| |
| const auto& devices = ddk_.get_devices(); |
| for (const auto& device : devices) { |
| const size_t idx = device->PowerDomainIndex(); |
| |
| // Find the cluster metadata that corresponds to this cluster index. |
| const auto& cluster_size_meta_itr = std::find_if( |
| std::begin(kClusterSizeMetadata), std::end(kClusterSizeMetadata), |
| [idx](const legacy_cluster_size_t& elem) -> bool { return idx == elem.pd_id; }); |
| |
| ASSERT_NE(cluster_size_meta_itr, std::end(kClusterSizeMetadata)); |
| ASSERT_EQ(cluster_size_meta_itr->core_count, device->ClusterCoreCount()); |
| } |
| } |
| |
| class AmlCpuTest : public AmlCpu { |
| public: |
| AmlCpuTest(ThermalSyncClient thermal) |
| : AmlCpu(nullptr, std::move(thermal), kBigClusterIdx, kBigClusterCoreCount) {} |
| |
| zx_status_t Init(); |
| static zx_status_t MessageOp(void* ctx, fidl_incoming_msg_t* msg, fidl_txn_t* txn); |
| zx::channel& GetMessengerChannel() { return messenger_.local(); } |
| |
| zx::vmo inspect_vmo() { return inspector_.DuplicateVmo(); } |
| |
| private: |
| fake_ddk::FidlMessenger messenger_; |
| }; |
| |
| zx_status_t AmlCpuTest::MessageOp(void* ctx, fidl_incoming_msg_t* msg, fidl_txn_t* txn) { |
| return static_cast<AmlCpuTest*>(ctx)->DdkMessage(msg, txn); |
| } |
| |
| zx_status_t AmlCpuTest::Init() { return messenger_.SetMessageOp(this, AmlCpuTest::MessageOp); } |
| |
| using inspect::InspectTestHelper; |
| |
| class AmlCpuTestFixture : public InspectTestHelper, public zxtest::Test { |
| public: |
| void SetUp() override; |
| |
| protected: |
| FakeAmlThermal thermal_; |
| |
| std::unique_ptr<AmlCpuTest> dut_; |
| std::unique_ptr<CpuCtrlSyncClient> cpu_client_; |
| }; |
| |
| void AmlCpuTestFixture::SetUp() { |
| ASSERT_OK(thermal_.Init({})); |
| ThermalSyncClient thermal_client(std::move(thermal_.GetMessengerChannel())); |
| |
| dut_ = std::make_unique<AmlCpuTest>(std::move(thermal_client)); |
| ASSERT_OK(dut_->Init()); |
| |
| cpu_client_ = std::make_unique<CpuCtrlSyncClient>(std::move(dut_->GetMessengerChannel())); |
| } |
| |
| TEST_F(AmlCpuTestFixture, TestGetPerformanceStateInfo) { |
| // Make sure that we can get information about all the supported pstates. |
| for (uint32_t i = 0; i < kFakeOperatingPoints.count; i++) { |
| auto pstateInfo = cpu_client_->GetPerformanceStateInfo(i); |
| |
| // First, make sure there were no transport errors. |
| ASSERT_OK(pstateInfo.status()); |
| |
| // Then make sure that the driver accepted the call. |
| ASSERT_FALSE(pstateInfo->result.is_err()); |
| |
| // Then make sure that we're getting the accepted frequency and voltage values. |
| EXPECT_EQ(pstateInfo->result.response().info.frequency_hz, |
| kFakeOperatingPoints.opp[kFakeOperatingPoints.count - i - 1].freq_hz); |
| EXPECT_EQ(pstateInfo->result.response().info.voltage_uv, |
| kFakeOperatingPoints.opp[kFakeOperatingPoints.count - i - 1].volt_uv); |
| } |
| |
| // Make sure that we can't get any information about pstates that don't |
| // exist. |
| for (uint32_t i = kFakeOperatingPoints.count; i < MAX_DEVICE_PERFORMANCE_STATES; i++) { |
| auto pstateInfo = cpu_client_->GetPerformanceStateInfo(i); |
| |
| // Even if it's an unsupported pstate, we still expect the transport to |
| // deliver the message successfully. |
| ASSERT_OK(pstateInfo.status()); |
| |
| // Make sure that the driver returns an error, however. |
| EXPECT_TRUE(pstateInfo->result.is_err()); |
| } |
| } |
| |
| TEST_F(AmlCpuTestFixture, TestSetPerformanceState) { |
| // Make sure that we can drive the CPU to all of the supported performance |
| // states. |
| for (uint32_t i = 0; i < kFakeOperatingPoints.count; i++) { |
| uint32_t out_state = UINT32_MAX; |
| zx_status_t st = dut_->DdkSetPerformanceState(i, &out_state); |
| |
| // Make sure the call succeeded. |
| EXPECT_OK(st); |
| |
| // Make sure we could actually drive the device into the state that we |
| // expected. |
| EXPECT_EQ(out_state, i); |
| |
| // Make sure that the call was forwarded to the thermal driver. |
| const uint16_t kExpectedOperatingPoint = |
| static_cast<uint16_t>(kFakeOperatingPoints.count - i - 1); |
| EXPECT_EQ(kExpectedOperatingPoint, thermal_.ActiveOperatingPoint()); |
| } |
| |
| // Next make sure that we can't drive the CPU into any unsupported |
| // performance states. |
| for (uint32_t i = kFakeOperatingPoints.count; i < MAX_DEVICE_PERFORMANCE_STATES; i++) { |
| const uint16_t kInitialOperatingPoint = thermal_.ActiveOperatingPoint(); |
| uint32_t out_state = UINT32_MAX; |
| zx_status_t st = dut_->DdkSetPerformanceState(i, &out_state); |
| |
| // This is not a supported performance state. |
| EXPECT_NOT_OK(st); |
| |
| // Make sure we haven't meddled with `out_state` |
| EXPECT_EQ(out_state, UINT32_MAX); |
| |
| // Make sure we haven't meddled with the thermal driver's active |
| // operating point. |
| EXPECT_EQ(kInitialOperatingPoint, thermal_.ActiveOperatingPoint()); |
| } |
| } |
| |
| TEST_F(AmlCpuTestFixture, TestSetCpuInfo) { |
| uint32_t test_cpu_version = 0x28200b02; |
| dut_->SetCpuInfo(test_cpu_version); |
| ASSERT_NO_FATAL_FAILURES(ReadInspect(dut_->inspect_vmo())); |
| auto* cpu_info = hierarchy().GetByPath({"cpu_info_service"}); |
| ASSERT_TRUE(cpu_info); |
| |
| // cpu_major_revision : 40 |
| ASSERT_NO_FATAL_FAILURES(CheckProperty<inspect::UintPropertyValue>( |
| cpu_info->node(), "cpu_major_revision", inspect::UintPropertyValue(40))); |
| // cpu_minor_revision : 11 |
| ASSERT_NO_FATAL_FAILURES(CheckProperty<inspect::UintPropertyValue>( |
| cpu_info->node(), "cpu_minor_revision", inspect::UintPropertyValue(11))); |
| // cpu_package_id : 2 |
| ASSERT_NO_FATAL_FAILURES(CheckProperty<inspect::UintPropertyValue>( |
| cpu_info->node(), "cpu_package_id", inspect::UintPropertyValue(2))); |
| } |
| |
| TEST_F(AmlCpuTestFixture, TestGetNumLogicalCores) { |
| auto resp = cpu_client_->GetNumLogicalCores(); |
| |
| ASSERT_OK(resp.status()); |
| |
| EXPECT_EQ(resp->count, kBigClusterCoreCount); |
| } |
| |
| } // namespace amlogic_cpu |
| |
| // Redefine PDevMakeMmioBufferWeak per the recommendation in pdev.h. |
| zx_status_t ddk::PDevMakeMmioBufferWeak(const pdev_mmio_t& pdev_mmio, |
| std::optional<MmioBuffer>* mmio, uint32_t cache_policy) { |
| auto* test_harness = reinterpret_cast<amlogic_cpu::FakeMmio*>(pdev_mmio.offset); |
| mmio->emplace(test_harness->mmio()); |
| return ZX_OK; |
| } |