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