blob: b4f4890c761881e1dea8a38d8d46e99939313801 [file] [log] [blame]
// Copyright (c) 2020 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "speed_select.hpp"
#include "cpuinfo.hpp"
#include "cpuinfo_utils.hpp"
#include <peci.h>
#include <boost/asio/error.hpp>
#include <boost/asio/steady_timer.hpp>
#include <xyz/openbmc_project/Common/Device/error.hpp>
#include <xyz/openbmc_project/Common/error.hpp>
#include <xyz/openbmc_project/Control/Processor/CurrentOperatingConfig/server.hpp>
#include <xyz/openbmc_project/Inventory/Item/Cpu/OperatingConfig/server.hpp>
#include <algorithm>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
namespace cpu_info
{
namespace sst
{
// Specialize char to print the integer value instead of ascii. We basically
// never want to print a single ascii char.
std::ostream& operator<<(std::ostream& os, uint8_t value)
{
return os << static_cast<int>(value);
}
bool checkPECIStatus(EPECIStatus libStatus, uint8_t completionCode)
{
if (libStatus != PECI_CC_SUCCESS || completionCode != PECI_DEV_CC_SUCCESS)
{
std::cerr << "PECI command failed."
<< " Driver Status = " << libStatus << ","
<< " Completion Code = " << completionCode << '\n';
return false;
}
return true;
}
std::vector<uint32_t> convertMaskToList(std::bitset<64> mask)
{
std::vector<uint32_t> bitList;
for (size_t i = 0; i < mask.size(); ++i)
{
if (mask.test(i))
{
bitList.push_back(i);
}
}
return bitList;
}
static std::vector<BackendProvider>& getProviders()
{
static auto* providers = new std::vector<BackendProvider>;
return *providers;
}
void registerBackend(BackendProvider providerFn)
{
getProviders().push_back(providerFn);
}
std::unique_ptr<SSTInterface> getInstance(uint8_t address, CPUModel model,
WakePolicy wakePolicy)
{
DEBUG_PRINT << "Searching for provider for " << address << ", model "
<< std::hex << model << std::dec << '\n';
for (const auto& provider : getProviders())
{
try
{
auto interface = provider(address, model, wakePolicy);
DEBUG_PRINT << "returned " << interface << '\n';
if (interface)
{
return interface;
}
}
catch (...)
{}
}
DEBUG_PRINT << "No supported backends found\n";
return nullptr;
}
using BaseCurrentOperatingConfig =
sdbusplus::server::object_t<sdbusplus::server::xyz::openbmc_project::
control::processor::CurrentOperatingConfig>;
using BaseOperatingConfig =
sdbusplus::server::object_t<sdbusplus::server::xyz::openbmc_project::
inventory::item::cpu::OperatingConfig>;
class OperatingConfig : public BaseOperatingConfig
{
public:
std::string path;
unsigned int level;
public:
using BaseOperatingConfig::BaseOperatingConfig;
OperatingConfig(sdbusplus::bus_t& bus, unsigned int level_,
std::string path_) :
BaseOperatingConfig(bus, path_.c_str(), action::defer_emit),
path(std::move(path_)), level(level_)
{}
};
class CPUConfig : public BaseCurrentOperatingConfig
{
private:
/** Objects describing all available SST configs - not modifiable. */
std::vector<std::unique_ptr<OperatingConfig>> availConfigs;
sdbusplus::bus_t& bus;
const uint8_t peciAddress;
const std::string path; ///< D-Bus path of CPU object
const CPUModel cpuModel;
// Keep mutable copies of the properties so we can cache values that we
// retrieve in the getters. We don't want to throw an error on a D-Bus
// get-property call (extra error handling in clients), so by caching we can
// hide any temporary hiccup in PECI communication.
// These values can be changed by in-band software so we have to do a full
// PECI read on every get-property, and can't assume that values will change
// only when set-property is done.
mutable unsigned int currentLevel;
mutable bool bfEnabled;
/**
* Enforce common pre-conditions for D-Bus set property handlers.
*/
void setPropertyCheckOrThrow(SSTInterface& sst)
{
if (!sst.supportsControl())
{
throw sdbusplus::xyz::openbmc_project::Common::Error::NotAllowed();
}
if (hostState != HostState::postComplete || !sst.ready())
{
throw sdbusplus::xyz::openbmc_project::Common::Error::Unavailable();
}
}
public:
CPUConfig(sdbusplus::bus_t& bus_, uint8_t index, CPUModel model,
unsigned int currentLevel_, bool bfEnabled_) :
BaseCurrentOperatingConfig(bus_, generatePath(index).c_str(),
action::defer_emit),
bus(bus_), peciAddress(index + MIN_CLIENT_ADDR),
path(generatePath(index)), cpuModel(model), currentLevel(currentLevel_),
bfEnabled(bfEnabled_)
{}
//
// D-Bus Property Overrides
//
sdbusplus::message::object_path appliedConfig() const override
{
DEBUG_PRINT << "Reading AppliedConfig\n";
if (hostState != HostState::off)
{
// Otherwise, try to read current state
auto sst = getInstance(peciAddress, cpuModel, dontWake);
if (!sst || !sst->ready())
{
std::cerr << __func__
<< ": Failed to get SST provider instance\n";
}
else
{
try
{
currentLevel = sst->currentLevel();
}
catch (const PECIError& error)
{
std::cerr << "Failed to get SST-PP level: " << error.what()
<< "\n";
}
}
}
return generateConfigPath(currentLevel);
}
bool baseSpeedPriorityEnabled() const override
{
DEBUG_PRINT << "Reading BaseSpeedPriorityEnabled\n";
if (hostState != HostState::off)
{
auto sst = getInstance(peciAddress, cpuModel, dontWake);
if (!sst || !sst->ready())
{
std::cerr << __func__
<< ": Failed to get SST provider instance\n";
}
else
{
try
{
bfEnabled = sst->bfEnabled(currentLevel);
}
catch (const PECIError& error)
{
std::cerr << "Failed to get SST-BF status: " << error.what()
<< "\n";
}
}
}
return bfEnabled;
}
sdbusplus::message::object_path
appliedConfig(sdbusplus::message::object_path value) override
{
DEBUG_PRINT << "Writing AppliedConfig\n";
const OperatingConfig* newConfig = nullptr;
for (const auto& config : availConfigs)
{
if (config->path == value.str)
{
newConfig = config.get();
}
}
if (newConfig == nullptr)
{
throw sdbusplus::xyz::openbmc_project::Common::Error::
InvalidArgument();
}
auto sst = getInstance(peciAddress, cpuModel, wakeAllowed);
if (!sst)
{
std::cerr << __func__ << ": Failed to get SST provider instance\n";
return sdbusplus::message::object_path();
}
try
{
setPropertyCheckOrThrow(*sst);
sst->setCurrentLevel(newConfig->level);
currentLevel = newConfig->level;
}
catch (const PECIError& error)
{
std::cerr << "Failed to set new SST-PP level: " << error.what()
<< "\n";
throw sdbusplus::xyz::openbmc_project::Common::Device::Error::
WriteFailure();
}
// return value not used
return sdbusplus::message::object_path();
}
bool baseSpeedPriorityEnabled(bool /* value */) override
{
DEBUG_PRINT << "Writing BaseSpeedPriorityEnabled not allowed\n";
throw sdbusplus::xyz::openbmc_project::Common::Error::NotAllowed();
// return value not used
return false;
}
//
// Additions
//
OperatingConfig& newConfig(unsigned int level)
{
availConfigs.emplace_back(std::make_unique<OperatingConfig>(
bus, level, generateConfigPath(level)));
return *availConfigs.back();
}
std::string generateConfigPath(unsigned int level) const
{
return path + "/config" + std::to_string(level);
}
/**
* Emit the interface added signals which were deferred. This is required
* for ObjectMapper to pick up the objects, if we initially deferred the
* signal emitting.
*/
void finalize()
{
emit_added();
for (auto& config : availConfigs)
{
config->emit_added();
}
}
static std::string generatePath(int index)
{
return cpuPath + std::to_string(index);
}
};
/**
* Retrieve the SST parameters for a single config and fill the values into the
* properties on the D-Bus interface.
*
* @param[in,out] sst Interface to SST backend.
* @param[in] level Config TDP level to retrieve.
* @param[out] config D-Bus interface to update.
*/
static void getSingleConfig(SSTInterface& sst, unsigned int level,
OperatingConfig& config)
{
config.powerLimit(sst.tdp(level));
DEBUG_PRINT << " TDP = " << config.powerLimit() << '\n';
config.availableCoreCount(sst.coreCount(level));
DEBUG_PRINT << " coreCount = " << config.availableCoreCount() << '\n';
config.baseSpeed(sst.p1Freq(level));
DEBUG_PRINT << " baseSpeed = " << config.baseSpeed() << '\n';
config.maxSpeed(sst.p0Freq(level));
DEBUG_PRINT << " maxSpeed = " << config.maxSpeed() << '\n';
config.maxJunctionTemperature(sst.prochotTemp(level));
DEBUG_PRINT << " procHot = " << config.maxJunctionTemperature() << '\n';
// Construct BaseSpeedPrioritySettings
std::vector<std::tuple<uint32_t, std::vector<uint32_t>>> baseSpeeds;
if (sst.bfSupported(level))
{
std::vector<uint32_t> totalCoreList, loFreqCoreList, hiFreqCoreList;
totalCoreList = sst.enabledCoreList(level);
hiFreqCoreList = sst.bfHighPriorityCoreList(level);
std::set_difference(
totalCoreList.begin(), totalCoreList.end(), hiFreqCoreList.begin(),
hiFreqCoreList.end(),
std::inserter(loFreqCoreList, loFreqCoreList.begin()));
baseSpeeds = {{sst.bfHighPriorityFreq(level), hiFreqCoreList},
{sst.bfLowPriorityFreq(level), loFreqCoreList}};
}
config.baseSpeedPrioritySettings(baseSpeeds);
config.turboProfile(sst.sseTurboProfile(level));
}
/**
* Retrieve all SST configuration info for all discoverable CPUs, and publish
* the info on new D-Bus objects on the given bus connection.
*
* @param[in,out] ioc ASIO context.
* @param[in,out] conn D-Bus ASIO connection.
*
* @return Whether discovery was successfully finished.
*
* @throw PECIError A PECI command failed on a CPU which had previously
* responded to a command.
*/
static bool discoverCPUsAndConfigs(boost::asio::io_context& ioc,
sdbusplus::asio::connection& conn)
{
// Persistent list - only populated after complete/successful discovery
static std::vector<std::unique_ptr<CPUConfig>> cpus;
cpus.clear();
// Temporary staging list. In case there is any failure, these temporary
// objects will get dropped to avoid presenting incomplete info until the
// next discovery attempt.
std::vector<std::unique_ptr<CPUConfig>> cpuList;
for (uint8_t i = MIN_CLIENT_ADDR; i <= MAX_CLIENT_ADDR; ++i)
{
// Let the event handler run any waiting tasks. If there is a lot of
// PECI contention, SST discovery could take a long time. This lets us
// get updates to hostState and handle any D-Bus requests.
ioc.poll();
if (hostState == HostState::off)
{
return false;
}
unsigned int cpuIndex = i - MIN_CLIENT_ADDR;
DEBUG_PRINT << "Discovering CPU " << cpuIndex << '\n';
// We could possibly check D-Bus for CPU presence and model, but PECI is
// 10x faster and so much simpler.
uint8_t cc, stepping;
CPUModel cpuModel;
EPECIStatus status = peci_GetCPUID(i, &cpuModel, &stepping, &cc);
if (status == PECI_CC_TIMEOUT)
{
// Timing out indicates the CPU is present but PCS services not
// working yet. Try again later.
throw PECIError("Get CPUID timed out");
}
if (status == PECI_CC_CPU_NOT_PRESENT)
{
continue;
}
if (status != PECI_CC_SUCCESS || cc != PECI_DEV_CC_SUCCESS)
{
std::cerr << "GetCPUID returned status " << status
<< ", cc = " << cc << '\n';
continue;
}
std::unique_ptr<SSTInterface> sst = getInstance(i, cpuModel,
wakeAllowed);
if (!sst)
{
// No supported backend for this CPU.
continue;
}
if (!sst->ready())
{
// Supported CPU but it can't be queried yet. Try again later.
std::cerr << "sst not ready yet\n";
return false;
}
if (!sst->ppEnabled())
{
// Supported CPU but the specific SKU doesn't support SST-PP.
std::cerr << "CPU doesn't support SST-PP\n";
continue;
}
// Create the per-CPU configuration object
unsigned int currentLevel = sst->currentLevel();
cpuList.emplace_back(
std::make_unique<CPUConfig>(conn, cpuIndex, cpuModel, currentLevel,
sst->bfEnabled(currentLevel)));
CPUConfig& cpu = *cpuList.back();
bool foundCurrentLevel = false;
for (unsigned int level = 0; level <= sst->maxLevel(); ++level)
{
DEBUG_PRINT << "checking level " << level << ": ";
// levels 1 and 2 were legacy/deprecated, originally used for AVX
// license pre-granting. They may be reused for more levels in
// future generations. So we need to check for discontinuities.
if (!sst->levelSupported(level))
{
DEBUG_PRINT << "not supported\n";
continue;
}
DEBUG_PRINT << "supported\n";
getSingleConfig(*sst, level, cpu.newConfig(level));
if (level == currentLevel)
{
foundCurrentLevel = true;
}
}
DEBUG_PRINT << "current level is " << currentLevel << '\n';
if (!foundCurrentLevel)
{
// In case we didn't encounter a PECI error, but also didn't find
// the config which is supposedly applied, we won't be able to
// populate the CurrentOperatingConfig so we have to remove this CPU
// from consideration.
std::cerr << "CPU " << cpuIndex
<< " claimed SST support but invalid configs\n";
cpuList.pop_back();
continue;
}
}
cpuList.swap(cpus);
std::for_each(cpus.begin(), cpus.end(), [](auto& cpu) { cpu->finalize(); });
return true;
}
/**
* Attempt discovery process, and if it fails, wait for 10 seconds to try again.
*/
static void discoverOrWait()
{
static boost::asio::steady_timer peciRetryTimer(dbus::getIOContext());
static int peciErrorCount = 0;
bool finished = false;
// This function may be called from hostStateHandler or by retrying itself.
// In case those overlap, cancel any outstanding retry timer.
peciRetryTimer.cancel();
try
{
DEBUG_PRINT << "Starting discovery\n";
finished = discoverCPUsAndConfigs(dbus::getIOContext(),
*dbus::getConnection());
}
catch (const PECIError& err)
{
std::cerr << "PECI Error: " << err.what() << '\n';
// In case of repeated failure to finish discovery, turn off this
// feature altogether. Possible cause is that the CPU model does not
// actually support the necessary commands.
if (++peciErrorCount >= 50)
{
std::cerr << "Aborting SST discovery\n";
return;
}
std::cerr << "Retrying SST discovery later\n";
}
DEBUG_PRINT << "Finished discovery attempt: " << finished << '\n';
// Retry later if no CPUs were available, or there was a PECI error.
if (!finished)
{
peciRetryTimer.expires_after(std::chrono::seconds(10));
peciRetryTimer.async_wait([](boost::system::error_code ec) {
if (ec)
{
if (ec != boost::asio::error::operation_aborted)
{
std::cerr << "SST PECI Retry Timer failed: " << ec << '\n';
}
return;
}
discoverOrWait();
});
}
}
static void hostStateHandler(HostState prevState, HostState)
{
if (prevState == HostState::off)
{
// Start or re-start discovery any time the host moves out of the
// powered off state.
discoverOrWait();
}
}
void init()
{
addHostStateCallback(hostStateHandler);
}
} // namespace sst
} // namespace cpu_info