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