1 /* 2 // Copyright (c) 2020 Intel Corporation 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 */ 16 17 #include "cpuinfo.hpp" 18 #include "cpuinfo_utils.hpp" 19 #include "speed_select.hpp" 20 21 #include <errno.h> 22 #include <fcntl.h> 23 #include <stdio.h> 24 #include <sys/ioctl.h> 25 26 #include <boost/asio/io_service.hpp> 27 #include <boost/asio/steady_timer.hpp> 28 29 #include <iostream> 30 #include <list> 31 #include <optional> 32 #include <sstream> 33 #include <string> 34 35 extern "C" 36 { 37 #include <i2c/smbus.h> 38 #include <linux/i2c-dev.h> 39 } 40 41 #include <peci.h> 42 43 #include <phosphor-logging/log.hpp> 44 #include <sdbusplus/asio/object_server.hpp> 45 46 namespace cpu_info 47 { 48 static constexpr bool debug = false; 49 static constexpr const char* assetInterfaceName = 50 "xyz.openbmc_project.Inventory.Decorator.Asset"; 51 static constexpr const char* cpuProcessName = 52 "xyz.openbmc_project.Smbios.MDR_V2"; 53 54 // constants for reading SSPEC or QDF string from PIROM 55 // Currently, they are the same for platforms with icx 56 static constexpr uint8_t defaultI2cBus = 13; 57 static constexpr uint8_t defaultI2cSlaveAddr0 = 0x50; 58 static constexpr uint8_t sspecRegAddr = 0xd; 59 static constexpr uint8_t sspecSize = 6; 60 61 using CPUInfoMap = boost::container::flat_map<size_t, std::shared_ptr<CPUInfo>>; 62 63 static CPUInfoMap cpuInfoMap = {}; 64 65 /** 66 * Simple aggregate to define an external D-Bus property which needs to be set 67 * by this application. 68 */ 69 struct CpuProperty 70 { 71 std::string object; 72 std::string interface; 73 std::string name; 74 std::string value; 75 }; 76 77 /** 78 * List of properties we want to set on other D-Bus objects. This list is kept 79 * around so that if any target objects are removed+readded, then we can set the 80 * values again. 81 */ 82 static std::list<CpuProperty> propertiesToSet; 83 84 static std::ostream& logStream(int cpu) 85 { 86 return std::cerr << "[CPU " << cpu << "] "; 87 } 88 89 static void 90 setCpuProperty(const std::shared_ptr<sdbusplus::asio::connection>& conn, 91 size_t cpu, const std::string& interface, 92 const std::string& propName, const std::string& propVal); 93 static void 94 setDbusProperty(const std::shared_ptr<sdbusplus::asio::connection>& conn, 95 size_t cpu, const CpuProperty& newProp); 96 static void createCpuUpdatedMatch( 97 const std::shared_ptr<sdbusplus::asio::connection>& conn, size_t cpu); 98 99 static std::optional<std::string> readSSpec(uint8_t bus, uint8_t slaveAddr, 100 uint8_t regAddr, size_t count) 101 { 102 unsigned long funcs = 0; 103 std::string devPath = "/dev/i2c-" + std::to_string(bus); 104 105 int fd = ::open(devPath.c_str(), O_RDWR); 106 if (fd < 0) 107 { 108 phosphor::logging::log<phosphor::logging::level::ERR>( 109 "Error in open!", 110 phosphor::logging::entry("PATH=%s", devPath.c_str()), 111 phosphor::logging::entry("SLAVEADDR=0x%x", slaveAddr)); 112 return std::nullopt; 113 } 114 115 if (::ioctl(fd, I2C_FUNCS, &funcs) < 0) 116 { 117 phosphor::logging::log<phosphor::logging::level::ERR>( 118 "Error in I2C_FUNCS!", 119 phosphor::logging::entry("PATH=%s", devPath.c_str()), 120 phosphor::logging::entry("SLAVEADDR=0x%x", slaveAddr)); 121 ::close(fd); 122 return std::nullopt; 123 } 124 125 if (!(funcs & I2C_FUNC_SMBUS_READ_BYTE_DATA)) 126 { 127 phosphor::logging::log<phosphor::logging::level::ERR>( 128 "i2c bus does not support read!", 129 phosphor::logging::entry("PATH=%s", devPath.c_str()), 130 phosphor::logging::entry("SLAVEADDR=0x%x", slaveAddr)); 131 ::close(fd); 132 return std::nullopt; 133 } 134 135 if (::ioctl(fd, I2C_SLAVE_FORCE, slaveAddr) < 0) 136 { 137 phosphor::logging::log<phosphor::logging::level::ERR>( 138 "Error in I2C_SLAVE_FORCE!", 139 phosphor::logging::entry("PATH=%s", devPath.c_str()), 140 phosphor::logging::entry("SLAVEADDR=0x%x", slaveAddr)); 141 ::close(fd); 142 return std::nullopt; 143 } 144 145 int value = 0; 146 std::string sspec; 147 sspec.reserve(count); 148 149 for (size_t i = 0; i < count; i++) 150 { 151 value = ::i2c_smbus_read_byte_data(fd, regAddr + i); 152 if (value < 0) 153 { 154 phosphor::logging::log<phosphor::logging::level::ERR>( 155 "Error in i2c read!", 156 phosphor::logging::entry("PATH=%s", devPath.c_str()), 157 phosphor::logging::entry("SLAVEADDR=0x%x", slaveAddr)); 158 ::close(fd); 159 return std::nullopt; 160 } 161 if (!std::isprint(static_cast<unsigned char>(value))) 162 { 163 phosphor::logging::log<phosphor::logging::level::ERR>( 164 "Non printable value in sspec, ignored."); 165 continue; 166 } 167 // sspec always starts with S, 168 // if not assume it is QDF string which starts at offset 2 169 if (i == 0 && static_cast<unsigned char>(value) != 'S') 170 { 171 i = 1; 172 continue; 173 } 174 sspec.push_back(static_cast<unsigned char>(value)); 175 } 176 ::close(fd); 177 178 if (sspec.size() < 4) 179 { 180 return std::nullopt; 181 } 182 183 return sspec; 184 } 185 186 /** 187 * Higher level SSpec logic. 188 * This handles retrying the PIROM reads until two subsequent reads are 189 * successful and return matching data. When we have confidence that the data 190 * read is correct, then set the property on D-Bus. 191 * 192 * @param[in,out] conn D-Bus connection. 193 * @param[in] cpuInfo CPU to read from. 194 */ 195 static void 196 tryReadSSpec(const std::shared_ptr<sdbusplus::asio::connection>& conn, 197 const std::shared_ptr<CPUInfo>& cpuInfo) 198 { 199 static int failedReads = 0; 200 201 std::optional<std::string> newSSpec = 202 readSSpec(cpuInfo->i2cBus, cpuInfo->i2cDevice, sspecRegAddr, sspecSize); 203 logStream(cpuInfo->id) << "SSpec read status: " 204 << static_cast<bool>(newSSpec) << "\n"; 205 if (newSSpec && newSSpec == cpuInfo->sSpec) 206 { 207 setCpuProperty(conn, cpuInfo->id, assetInterfaceName, "Model", 208 *newSSpec); 209 return; 210 } 211 212 // If this read failed, back off for a little longer so that hopefully the 213 // transient condition affecting PIROM reads will pass, but give up after 214 // several consecutive failures. But if this read looked OK, try again 215 // sooner to confirm it. 216 int retrySeconds; 217 if (newSSpec) 218 { 219 retrySeconds = 1; 220 failedReads = 0; 221 cpuInfo->sSpec = *newSSpec; 222 } 223 else 224 { 225 retrySeconds = 5; 226 if (++failedReads > 10) 227 { 228 logStream(cpuInfo->id) << "PIROM Read failed too many times\n"; 229 return; 230 } 231 } 232 233 auto sspecTimer = std::make_shared<boost::asio::steady_timer>( 234 conn->get_io_context(), std::chrono::seconds(retrySeconds)); 235 sspecTimer->async_wait( 236 [sspecTimer, conn, cpuInfo](boost::system::error_code ec) { 237 if (ec) 238 { 239 return; 240 } 241 tryReadSSpec(conn, cpuInfo); 242 }); 243 } 244 245 /** 246 * Add a D-Bus property to the global list, and attempt to set it by calling 247 * `setDbusProperty`. 248 * 249 * @param[in,out] conn D-Bus connection. 250 * @param[in] cpu 1-based CPU index. 251 * @param[in] interface Interface to set. 252 * @param[in] propName Property to set. 253 * @param[in] propVal Value to set. 254 */ 255 static void 256 setCpuProperty(const std::shared_ptr<sdbusplus::asio::connection>& conn, 257 size_t cpu, const std::string& interface, 258 const std::string& propName, const std::string& propVal) 259 { 260 // cpuId from configuration is one based as 261 // dbus object path used by smbios is 0 based 262 const std::string objectPath = cpuPath + std::to_string(cpu - 1); 263 264 // Can switch to emplace_back if you define a CpuProperty constructor. 265 propertiesToSet.push_back( 266 CpuProperty{objectPath, interface, propName, propVal}); 267 268 setDbusProperty(conn, cpu, propertiesToSet.back()); 269 } 270 271 /** 272 * Set a D-Bus property which is already contained in the global list, and also 273 * setup a D-Bus match to make sure the target property stays correct. 274 * 275 * @param[in,out] conn D-Bus connection. 276 * @param[in] cpu 1-baesd CPU index. 277 * @param[in] newProp Property to set. 278 */ 279 static void 280 setDbusProperty(const std::shared_ptr<sdbusplus::asio::connection>& conn, 281 size_t cpu, const CpuProperty& newProp) 282 { 283 createCpuUpdatedMatch(conn, cpu); 284 conn->async_method_call( 285 [](const boost::system::error_code ec) { 286 if (ec) 287 { 288 phosphor::logging::log<phosphor::logging::level::ERR>( 289 "Cannot set CPU property!"); 290 return; 291 } 292 }, 293 cpuProcessName, newProp.object.c_str(), 294 "org.freedesktop.DBus.Properties", "Set", newProp.interface, 295 newProp.name, std::variant<std::string>{newProp.value}); 296 } 297 298 /** 299 * Set up a D-Bus match (if one does not already exist) to watch for any new 300 * interfaces on the cpu object. When new interfaces are added, re-send all 301 * properties targeting that object/interface. 302 * 303 * @param[in,out] conn D-Bus connection. 304 * @param[in] cpu 1-based CPU index. 305 */ 306 static void createCpuUpdatedMatch( 307 const std::shared_ptr<sdbusplus::asio::connection>& conn, size_t cpu) 308 { 309 static boost::container::flat_map<size_t, 310 std::unique_ptr<sdbusplus::bus::match_t>> 311 cpuUpdatedMatch; 312 313 if (cpuUpdatedMatch[cpu]) 314 { 315 return; 316 } 317 318 const std::string objectPath = cpuPath + std::to_string(cpu - 1); 319 320 cpuUpdatedMatch.insert_or_assign( 321 cpu, 322 std::make_unique<sdbusplus::bus::match::match>( 323 static_cast<sdbusplus::bus::bus&>(*conn), 324 sdbusplus::bus::match::rules::interfacesAdded() + 325 sdbusplus::bus::match::rules::argNpath(0, objectPath.c_str()), 326 [conn, cpu](sdbusplus::message::message& msg) { 327 sdbusplus::message::object_path objectName; 328 boost::container::flat_map< 329 std::string, 330 boost::container::flat_map< 331 std::string, std::variant<std::string, uint64_t>>> 332 msgData; 333 334 msg.read(objectName, msgData); 335 336 // Go through all the property changes, and retry all of them 337 // targeting this object/interface which was just added. 338 for (const CpuProperty& prop : propertiesToSet) 339 { 340 if (prop.object == objectName && 341 msgData.contains(prop.interface)) 342 { 343 setDbusProperty(conn, cpu, prop); 344 } 345 } 346 })); 347 } 348 349 static void 350 getProcessorInfo(boost::asio::io_service& io, 351 const std::shared_ptr<sdbusplus::asio::connection>& conn, 352 const size_t& cpu) 353 { 354 if (cpuInfoMap.find(cpu) == cpuInfoMap.end() || cpuInfoMap[cpu] == nullptr) 355 { 356 std::cerr << "No information found for cpu " << cpu << "\n"; 357 return; 358 } 359 360 std::shared_ptr<CPUInfo> cpuInfo = cpuInfoMap[cpu]; 361 362 if (cpuInfo->id != cpu) 363 { 364 std::cerr << "Incorrect CPU id " << (unsigned)cpuInfo->id << " expect " 365 << cpu << "\n"; 366 return; 367 } 368 369 uint8_t cpuAddr = cpuInfo->peciAddr; 370 371 uint8_t cc = 0; 372 CPUModel model{}; 373 uint8_t stepping = 0; 374 375 // Wait for POST to complete to ensure that BIOS has time to enable the 376 // PPIN. Before BIOS enables it, we would get a 0x90 CC on PECI. 377 if (hostState != HostState::postComplete || 378 peci_GetCPUID(cpuAddr, &model, &stepping, &cc) != PECI_CC_SUCCESS) 379 { 380 // Start the PECI check loop 381 auto waitTimer = std::make_shared<boost::asio::steady_timer>(io); 382 waitTimer->expires_after( 383 std::chrono::seconds(cpu_info::peciCheckInterval)); 384 385 waitTimer->async_wait( 386 [waitTimer, &io, conn, cpu](const boost::system::error_code& ec) { 387 if (ec) 388 { 389 // operation_aborted is expected if timer is canceled 390 // before completion. 391 if (ec != boost::asio::error::operation_aborted) 392 { 393 phosphor::logging::log<phosphor::logging::level::ERR>( 394 "info update timer async_wait failed ", 395 phosphor::logging::entry("EC=0x%x", ec.value())); 396 } 397 return; 398 } 399 getProcessorInfo(io, conn, cpu); 400 }); 401 return; 402 } 403 404 switch (model) 405 { 406 case icx: 407 case icxd: 408 case spr: 409 { 410 // PPIN can be read through PCS 19 411 static constexpr uint8_t u8Size = 4; // default to a DWORD 412 static constexpr uint8_t u8PPINPkgIndex = 19; 413 static constexpr uint16_t u16PPINPkgParamHigh = 2; 414 static constexpr uint16_t u16PPINPkgParamLow = 1; 415 uint64_t cpuPPIN = 0; 416 uint32_t u32PkgValue = 0; 417 418 int ret = 419 peci_RdPkgConfig(cpuAddr, u8PPINPkgIndex, u16PPINPkgParamLow, 420 u8Size, (uint8_t*)&u32PkgValue, &cc); 421 if (0 != ret) 422 { 423 phosphor::logging::log<phosphor::logging::level::ERR>( 424 "peci read package config failed at address", 425 phosphor::logging::entry("PECIADDR=0x%x", 426 (unsigned)cpuAddr), 427 phosphor::logging::entry("CC=0x%x", cc)); 428 u32PkgValue = 0; 429 } 430 431 cpuPPIN = u32PkgValue; 432 ret = peci_RdPkgConfig(cpuAddr, u8PPINPkgIndex, u16PPINPkgParamHigh, 433 u8Size, (uint8_t*)&u32PkgValue, &cc); 434 if (0 != ret) 435 { 436 phosphor::logging::log<phosphor::logging::level::ERR>( 437 "peci read package config failed at address", 438 phosphor::logging::entry("PECIADDR=0x%x", 439 (unsigned)cpuAddr), 440 phosphor::logging::entry("CC=0x%x", cc)); 441 cpuPPIN = 0; 442 u32PkgValue = 0; 443 } 444 445 cpuPPIN |= static_cast<uint64_t>(u32PkgValue) << 32; 446 447 // set SerialNumber if cpuPPIN is valid 448 if (0 != cpuPPIN) 449 { 450 std::stringstream stream; 451 stream << std::hex << cpuPPIN; 452 std::string serialNumber(stream.str()); 453 cpuInfo->uniqueIdentifier(serialNumber); 454 // Signal that the iface is added now so that ObjectMapper and 455 // others can find it. 456 cpuInfo->emit_added(); 457 } 458 459 tryReadSSpec(conn, cpuInfo); 460 break; 461 } 462 default: 463 phosphor::logging::log<phosphor::logging::level::INFO>( 464 "in-compatible cpu for cpu asset info"); 465 break; 466 } 467 } 468 469 /** 470 * Get cpu and pirom address 471 */ 472 static void 473 getCpuAddress(boost::asio::io_service& io, 474 const std::shared_ptr<sdbusplus::asio::connection>& conn, 475 const std::string& service, const std::string& object, 476 const std::string& interface) 477 { 478 conn->async_method_call( 479 [&io, conn](boost::system::error_code ec, 480 const boost::container::flat_map< 481 std::string, 482 std::variant<std::string, uint64_t, uint32_t, uint16_t, 483 std::vector<std::string>>>& properties) { 484 const uint64_t* value = nullptr; 485 uint8_t peciAddress = 0; 486 uint8_t i2cBus = defaultI2cBus; 487 uint8_t i2cDevice; 488 bool i2cDeviceFound = false; 489 size_t cpu = 0; 490 491 if (ec) 492 { 493 std::cerr << "DBUS response error " << ec.value() << ": " 494 << ec.message() << "\n"; 495 return; 496 } 497 498 for (const auto& property : properties) 499 { 500 std::cerr << "property " << property.first << "\n"; 501 if (property.first == "Address") 502 { 503 value = std::get_if<uint64_t>(&property.second); 504 if (value != nullptr) 505 { 506 peciAddress = static_cast<uint8_t>(*value); 507 } 508 } 509 if (property.first == "CpuID") 510 { 511 value = std::get_if<uint64_t>(&property.second); 512 if (value != nullptr) 513 { 514 cpu = static_cast<size_t>(*value); 515 } 516 } 517 if (property.first == "PiromI2cAddress") 518 { 519 value = std::get_if<uint64_t>(&property.second); 520 if (value != nullptr) 521 { 522 i2cDevice = static_cast<uint8_t>(*value); 523 i2cDeviceFound = true; 524 } 525 } 526 if (property.first == "PiromI2cBus") 527 { 528 value = std::get_if<uint64_t>(&property.second); 529 if (value != nullptr) 530 { 531 i2cBus = static_cast<uint8_t>(*value); 532 } 533 } 534 } 535 536 ///\todo replace this with present + power state 537 if (cpu != 0 && peciAddress != 0) 538 { 539 if (!i2cDeviceFound) 540 { 541 i2cDevice = defaultI2cSlaveAddr0 + cpu - 1; 542 } 543 cpuInfoMap.insert_or_assign( 544 cpu, std::make_shared<CPUInfo>(*conn, cpu, peciAddress, 545 i2cBus, i2cDevice)); 546 547 getProcessorInfo(io, conn, cpu); 548 } 549 }, 550 service, object, "org.freedesktop.DBus.Properties", "GetAll", 551 interface); 552 } 553 554 /** 555 * D-Bus client: to get platform specific configs 556 */ 557 static void getCpuConfiguration( 558 boost::asio::io_service& io, 559 const std::shared_ptr<sdbusplus::asio::connection>& conn, 560 sdbusplus::asio::object_server& objServer) 561 { 562 // Get the Cpu configuration 563 // In case it's not available, set a match for it 564 static std::unique_ptr<sdbusplus::bus::match::match> cpuConfigMatch = 565 std::make_unique<sdbusplus::bus::match::match>( 566 *conn, 567 "type='signal',interface='org.freedesktop.DBus.Properties',member='" 568 "PropertiesChanged',arg0='xyz.openbmc_project." 569 "Configuration.XeonCPU'", 570 [&io, conn, &objServer](sdbusplus::message::message& msg) { 571 std::cerr << "get cpu configuration match\n"; 572 static boost::asio::steady_timer filterTimer(io); 573 filterTimer.expires_after( 574 std::chrono::seconds(configCheckInterval)); 575 576 filterTimer.async_wait( 577 [&io, conn, 578 &objServer](const boost::system::error_code& ec) { 579 if (ec == boost::asio::error::operation_aborted) 580 { 581 return; // we're being canceled 582 } 583 else if (ec) 584 { 585 std::cerr << "Error: " << ec.message() << "\n"; 586 return; 587 } 588 getCpuConfiguration(io, conn, objServer); 589 }); 590 }); 591 592 conn->async_method_call( 593 [&io, conn]( 594 boost::system::error_code ec, 595 const std::vector<std::pair< 596 std::string, 597 std::vector<std::pair<std::string, std::vector<std::string>>>>>& 598 subtree) { 599 if constexpr (debug) 600 std::cerr << "async_method_call callback\n"; 601 602 if (ec) 603 { 604 std::cerr << "error with async_method_call\n"; 605 return; 606 } 607 if (subtree.empty()) 608 { 609 // No config data yet, so wait for the match 610 return; 611 } 612 613 for (const auto& object : subtree) 614 { 615 for (const auto& service : object.second) 616 { 617 getCpuAddress(io, conn, service.first, object.first, 618 "xyz.openbmc_project.Configuration.XeonCPU"); 619 } 620 } 621 if constexpr (debug) 622 std::cerr << "getCpuConfiguration callback complete\n"; 623 624 return; 625 }, 626 "xyz.openbmc_project.ObjectMapper", 627 "/xyz/openbmc_project/object_mapper", 628 "xyz.openbmc_project.ObjectMapper", "GetSubTree", 629 "/xyz/openbmc_project/", 0, 630 std::array<const char*, 1>{ 631 "xyz.openbmc_project.Configuration.XeonCPU"}); 632 } 633 634 } // namespace cpu_info 635 636 int main(int argc, char* argv[]) 637 { 638 // setup connection to dbus 639 boost::asio::io_service& io = cpu_info::dbus::getIOContext(); 640 std::shared_ptr<sdbusplus::asio::connection> conn = 641 cpu_info::dbus::getConnection(); 642 643 // CPUInfo Object 644 conn->request_name(cpu_info::cpuInfoObject); 645 sdbusplus::asio::object_server server = 646 sdbusplus::asio::object_server(conn); 647 sdbusplus::bus::bus& bus = static_cast<sdbusplus::bus::bus&>(*conn); 648 sdbusplus::server::manager::manager objManager( 649 bus, "/xyz/openbmc_project/inventory"); 650 651 cpu_info::hostStateSetup(conn); 652 653 cpu_info::sst::init(io, conn); 654 655 // shared_ptr conn is global for the service 656 // const reference of conn is passed to async calls 657 cpu_info::getCpuConfiguration(io, conn, server); 658 659 io.run(); 660 661 return 0; 662 } 663