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+readded, then we can set the 84 * 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> readSSpec(uint8_t bus, uint8_t slaveAddr, 104 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 197 tryReadSSpec(const std::shared_ptr<sdbusplus::asio::connection>& conn, 198 size_t cpuIndex) 199 { 200 static int failedReads = 0; 201 202 auto cpuInfoIt = cpuInfoMap.find(cpuIndex); 203 if (cpuInfoIt == cpuInfoMap.end()) 204 { 205 return; 206 } 207 auto cpuInfo = cpuInfoIt->second; 208 209 std::optional<std::string> newSSpec = 210 readSSpec(cpuInfo->i2cBus, cpuInfo->i2cDevice, sspecRegAddr, sspecSize); 211 logStream(cpuInfo->id) << "SSpec read status: " 212 << static_cast<bool>(newSSpec) << "\n"; 213 if (newSSpec && newSSpec == cpuInfo->sSpec) 214 { 215 setCpuProperty(conn, cpuInfo->id, assetInterfaceName, "Model", 216 *newSSpec); 217 return; 218 } 219 220 // If this read failed, back off for a little longer so that hopefully the 221 // transient condition affecting PIROM reads will pass, but give up after 222 // several consecutive failures. But if this read looked OK, try again 223 // sooner to confirm it. 224 int retrySeconds; 225 if (newSSpec) 226 { 227 retrySeconds = 1; 228 failedReads = 0; 229 cpuInfo->sSpec = *newSSpec; 230 } 231 else 232 { 233 retrySeconds = 5; 234 if (++failedReads > 10) 235 { 236 logStream(cpuInfo->id) << "PIROM Read failed too many times\n"; 237 return; 238 } 239 } 240 241 auto sspecTimer = std::make_shared<boost::asio::steady_timer>( 242 conn->get_io_context(), std::chrono::seconds(retrySeconds)); 243 sspecTimer->async_wait( 244 [sspecTimer, conn, cpuIndex](boost::system::error_code ec) { 245 if (ec) 246 { 247 return; 248 } 249 tryReadSSpec(conn, cpuIndex); 250 }); 251 } 252 253 /** 254 * Add a D-Bus property to the global list, and attempt to set it by calling 255 * `setDbusProperty`. 256 * 257 * @param[in,out] conn D-Bus connection. 258 * @param[in] cpu 1-based CPU index. 259 * @param[in] interface Interface to set. 260 * @param[in] propName Property to set. 261 * @param[in] propVal Value to set. 262 */ 263 static void 264 setCpuProperty(const std::shared_ptr<sdbusplus::asio::connection>& conn, 265 size_t cpu, const std::string& interface, 266 const std::string& propName, const std::string& propVal) 267 { 268 // cpuId from configuration is one based as 269 // dbus object path used by smbios is 0 based 270 const std::string objectPath = cpuPath + std::to_string(cpu - 1); 271 272 // Can switch to emplace_back if you define a CpuProperty constructor. 273 propertiesToSet.push_back( 274 CpuProperty{objectPath, interface, propName, propVal}); 275 276 setDbusProperty(conn, cpu, propertiesToSet.back()); 277 } 278 279 /** 280 * Set a D-Bus property which is already contained in the global list, and also 281 * setup a D-Bus match to make sure the target property stays correct. 282 * 283 * @param[in,out] conn D-Bus connection. 284 * @param[in] cpu 1-baesd CPU index. 285 * @param[in] newProp Property to set. 286 */ 287 static void 288 setDbusProperty(const std::shared_ptr<sdbusplus::asio::connection>& conn, 289 size_t cpu, const CpuProperty& newProp) 290 { 291 createCpuUpdatedMatch(conn, cpu); 292 conn->async_method_call( 293 [](const boost::system::error_code ec) { 294 if (ec) 295 { 296 phosphor::logging::log<phosphor::logging::level::ERR>( 297 "Cannot set CPU property!"); 298 return; 299 } 300 }, 301 cpuProcessName, newProp.object.c_str(), 302 "org.freedesktop.DBus.Properties", "Set", newProp.interface, 303 newProp.name, std::variant<std::string>{newProp.value}); 304 } 305 306 /** 307 * Set up a D-Bus match (if one does not already exist) to watch for any new 308 * interfaces on the cpu object. When new interfaces are added, re-send all 309 * properties targeting that object/interface. 310 * 311 * @param[in,out] conn D-Bus connection. 312 * @param[in] cpu 1-based CPU index. 313 */ 314 static void createCpuUpdatedMatch( 315 const std::shared_ptr<sdbusplus::asio::connection>& conn, size_t cpu) 316 { 317 static boost::container::flat_map<size_t, 318 std::unique_ptr<sdbusplus::bus::match_t>> 319 cpuUpdatedMatch; 320 321 if (cpuUpdatedMatch[cpu]) 322 { 323 return; 324 } 325 326 const std::string objectPath = cpuPath + std::to_string(cpu - 1); 327 328 cpuUpdatedMatch.insert_or_assign( 329 cpu, 330 std::make_unique<sdbusplus::bus::match_t>( 331 static_cast<sdbusplus::bus_t&>(*conn), 332 sdbusplus::bus::match::rules::interfacesAdded() + 333 sdbusplus::bus::match::rules::argNpath(0, objectPath.c_str()), 334 [conn, cpu](sdbusplus::message_t& msg) { 335 sdbusplus::message::object_path objectName; 336 boost::container::flat_map< 337 std::string, 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 && msgData.contains(prop.interface)) 348 { 349 setDbusProperty(conn, cpu, prop); 350 } 351 } 352 })); 353 } 354 355 #if PECI_ENABLED 356 static void getPPIN(boost::asio::io_service& io, 357 const std::shared_ptr<sdbusplus::asio::connection>& conn, 358 const size_t& cpu) 359 { 360 if (cpuInfoMap.find(cpu) == cpuInfoMap.end() || cpuInfoMap[cpu] == nullptr) 361 { 362 std::cerr << "No information found for cpu " << cpu << "\n"; 363 return; 364 } 365 366 std::shared_ptr<CPUInfo> cpuInfo = cpuInfoMap[cpu]; 367 368 if (cpuInfo->id != cpu) 369 { 370 std::cerr << "Incorrect CPU id " << (unsigned)cpuInfo->id << " expect " 371 << cpu << "\n"; 372 return; 373 } 374 375 uint8_t cpuAddr = cpuInfo->peciAddr; 376 377 uint8_t cc = 0; 378 CPUModel model{}; 379 uint8_t stepping = 0; 380 381 // Wait for POST to complete to ensure that BIOS has time to enable the 382 // PPIN. Before BIOS enables it, we would get a 0x90 CC on PECI. 383 if (hostState != HostState::postComplete || 384 peci_GetCPUID(cpuAddr, &model, &stepping, &cc) != PECI_CC_SUCCESS) 385 { 386 // Start the PECI check loop 387 auto waitTimer = std::make_shared<boost::asio::steady_timer>(io); 388 waitTimer->expires_after( 389 std::chrono::seconds(cpu_info::peciCheckInterval)); 390 391 waitTimer->async_wait( 392 [waitTimer, &io, conn, cpu](const boost::system::error_code& ec) { 393 if (ec) 394 { 395 // operation_aborted is expected if timer is canceled 396 // before completion. 397 if (ec != boost::asio::error::operation_aborted) 398 { 399 phosphor::logging::log<phosphor::logging::level::ERR>( 400 "info update timer async_wait failed ", 401 phosphor::logging::entry("EC=0x%x", ec.value())); 402 } 403 return; 404 } 405 getPPIN(io, conn, cpu); 406 }); 407 return; 408 } 409 410 switch (model) 411 { 412 case iceLake: 413 case iceLakeD: 414 case sapphireRapids: 415 case emeraldRapids: 416 case graniteRapids: 417 case graniteRapidsD: 418 case sierraForest: 419 { 420 // PPIN can be read through PCS 19 421 static constexpr uint8_t u8Size = 4; // default to a DWORD 422 static constexpr uint8_t u8PPINPkgIndex = 19; 423 static constexpr uint16_t u16PPINPkgParamHigh = 2; 424 static constexpr uint16_t u16PPINPkgParamLow = 1; 425 uint64_t cpuPPIN = 0; 426 uint32_t u32PkgValue = 0; 427 428 int ret = peci_RdPkgConfig(cpuAddr, u8PPINPkgIndex, 429 u16PPINPkgParamLow, u8Size, 430 (uint8_t*)&u32PkgValue, &cc); 431 if (0 != ret) 432 { 433 phosphor::logging::log<phosphor::logging::level::ERR>( 434 "peci read package config failed at address", 435 phosphor::logging::entry("PECIADDR=0x%x", 436 (unsigned)cpuAddr), 437 phosphor::logging::entry("CC=0x%x", cc)); 438 u32PkgValue = 0; 439 } 440 441 cpuPPIN = u32PkgValue; 442 ret = peci_RdPkgConfig(cpuAddr, u8PPINPkgIndex, u16PPINPkgParamHigh, 443 u8Size, (uint8_t*)&u32PkgValue, &cc); 444 if (0 != ret) 445 { 446 phosphor::logging::log<phosphor::logging::level::ERR>( 447 "peci read package config failed at address", 448 phosphor::logging::entry("PECIADDR=0x%x", 449 (unsigned)cpuAddr), 450 phosphor::logging::entry("CC=0x%x", cc)); 451 cpuPPIN = 0; 452 u32PkgValue = 0; 453 } 454 455 cpuPPIN |= static_cast<uint64_t>(u32PkgValue) << 32; 456 457 // set SerialNumber if cpuPPIN is valid 458 if (0 != cpuPPIN) 459 { 460 std::stringstream stream; 461 stream << std::hex << cpuPPIN; 462 std::string serialNumber(stream.str()); 463 cpuInfo->publishUUID(*conn, serialNumber); 464 } 465 break; 466 } 467 default: 468 phosphor::logging::log<phosphor::logging::level::INFO>( 469 "in-compatible cpu for cpu asset info"); 470 break; 471 } 472 } 473 #endif 474 475 /** 476 * Get cpu and pirom address 477 */ 478 static void 479 getCpuAddress(boost::asio::io_service& io, 480 const std::shared_ptr<sdbusplus::asio::connection>& conn, 481 const std::string& service, const std::string& object, 482 const std::string& interface) 483 { 484 conn->async_method_call( 485 [&io, conn](boost::system::error_code ec, 486 const boost::container::flat_map< 487 std::string, 488 std::variant<std::string, uint64_t, uint32_t, uint16_t, 489 std::vector<std::string>>>& properties) { 490 const uint64_t* value = nullptr; 491 std::optional<uint8_t> peciAddress; 492 uint8_t i2cBus = defaultI2cBus; 493 std::optional<uint8_t> i2cDevice; 494 std::optional<size_t> cpu; 495 496 if (ec) 497 { 498 std::cerr << "DBUS response error " << ec.value() << ": " 499 << ec.message() << "\n"; 500 return; 501 } 502 503 for (const auto& property : properties) 504 { 505 std::cerr << "property " << property.first << "\n"; 506 if (property.first == "Address") 507 { 508 value = std::get_if<uint64_t>(&property.second); 509 if (value != nullptr) 510 { 511 peciAddress = static_cast<uint8_t>(*value); 512 } 513 } 514 if (property.first == "CpuID") 515 { 516 value = std::get_if<uint64_t>(&property.second); 517 if (value != nullptr) 518 { 519 cpu = static_cast<size_t>(*value); 520 } 521 } 522 if (property.first == "PiromI2cAddress") 523 { 524 value = std::get_if<uint64_t>(&property.second); 525 if (value != nullptr) 526 { 527 i2cDevice = static_cast<uint8_t>(*value); 528 } 529 } 530 if (property.first == "PiromI2cBus") 531 { 532 value = std::get_if<uint64_t>(&property.second); 533 if (value != nullptr) 534 { 535 i2cBus = static_cast<uint8_t>(*value); 536 } 537 } 538 } 539 540 if (!cpu || !peciAddress) 541 { 542 return; 543 } 544 545 if (!i2cDevice) 546 { 547 i2cDevice = defaultI2cSlaveAddr0 + *cpu - 1; 548 } 549 550 auto key = cpuInfoMap.find(*cpu); 551 552 if (key != cpuInfoMap.end()) 553 { 554 cpuInfoMap.erase(key); 555 } 556 557 cpuInfoMap.emplace(*cpu, std::make_shared<CPUInfo>(*cpu, *peciAddress, 558 i2cBus, *i2cDevice)); 559 560 tryReadSSpec(conn, *cpu); 561 562 #if PECI_ENABLED 563 getPPIN(io, conn, *cpu); 564 #endif 565 }, 566 service, object, "org.freedesktop.DBus.Properties", "GetAll", 567 interface); 568 } 569 570 /** 571 * D-Bus client: to get platform specific configs 572 */ 573 static void getCpuConfiguration( 574 boost::asio::io_service& io, 575 const std::shared_ptr<sdbusplus::asio::connection>& conn, 576 sdbusplus::asio::object_server& objServer) 577 { 578 // Get the Cpu configuration 579 // In case it's not available, set a match for it 580 static std::unique_ptr<sdbusplus::bus::match_t> cpuConfigMatch = 581 std::make_unique<sdbusplus::bus::match_t>( 582 *conn, 583 "type='signal',interface='org.freedesktop.DBus.Properties',member='" 584 "PropertiesChanged',arg0='xyz.openbmc_project." 585 "Configuration.XeonCPU'", 586 [&io, conn, &objServer](sdbusplus::message_t& /* msg */) { 587 std::cerr << "get cpu configuration match\n"; 588 static boost::asio::steady_timer filterTimer(io); 589 filterTimer.expires_after(std::chrono::seconds(configCheckInterval)); 590 591 filterTimer.async_wait( 592 [&io, conn, &objServer](const boost::system::error_code& ec) { 593 if (ec == boost::asio::error::operation_aborted) 594 { 595 return; // we're being canceled 596 } 597 else if (ec) 598 { 599 std::cerr << "Error: " << ec.message() << "\n"; 600 return; 601 } 602 getCpuConfiguration(io, conn, objServer); 603 }); 604 }); 605 606 conn->async_method_call( 607 [&io, conn]( 608 boost::system::error_code ec, 609 const std::vector<std::pair< 610 std::string, 611 std::vector<std::pair<std::string, std::vector<std::string>>>>>& 612 subtree) { 613 if constexpr (debug) 614 std::cerr << "async_method_call callback\n"; 615 616 if (ec) 617 { 618 std::cerr << "error with async_method_call\n"; 619 return; 620 } 621 if (subtree.empty()) 622 { 623 // No config data yet, so wait for the match 624 return; 625 } 626 627 for (const auto& object : subtree) 628 { 629 for (const auto& service : object.second) 630 { 631 getCpuAddress(io, conn, service.first, object.first, 632 "xyz.openbmc_project.Configuration.XeonCPU"); 633 } 634 } 635 if constexpr (debug) 636 std::cerr << "getCpuConfiguration callback complete\n"; 637 638 return; 639 }, 640 "xyz.openbmc_project.ObjectMapper", 641 "/xyz/openbmc_project/object_mapper", 642 "xyz.openbmc_project.ObjectMapper", "GetSubTree", 643 "/xyz/openbmc_project/", 0, 644 std::array<const char*, 1>{ 645 "xyz.openbmc_project.Configuration.XeonCPU"}); 646 } 647 648 } // namespace cpu_info 649 650 int main() 651 { 652 // setup connection to dbus 653 boost::asio::io_service& io = cpu_info::dbus::getIOContext(); 654 std::shared_ptr<sdbusplus::asio::connection> conn = 655 cpu_info::dbus::getConnection(); 656 657 // CPUInfo Object 658 conn->request_name(cpu_info::cpuInfoObject); 659 sdbusplus::asio::object_server server = 660 sdbusplus::asio::object_server(conn); 661 sdbusplus::bus_t& bus = static_cast<sdbusplus::bus_t&>(*conn); 662 sdbusplus::server::manager_t objManager(bus, 663 "/xyz/openbmc_project/inventory"); 664 665 cpu_info::hostStateSetup(conn); 666 667 #if PECI_ENABLED 668 cpu_info::sst::init(); 669 #endif 670 671 // shared_ptr conn is global for the service 672 // const reference of conn is passed to async calls 673 cpu_info::getCpuConfiguration(io, conn, server); 674 675 io.run(); 676 677 return 0; 678 } 679