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
logStream(int cpu)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,uint8_t regAddr,size_t count)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 */
tryReadSSpec(const std::shared_ptr<sdbusplus::asio::connection> & conn,size_t cpuIndex)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
setCpuProperty(const std::shared_ptr<sdbusplus::asio::connection> & conn,size_t cpu,const std::string & interface,const std::string & propName,const std::string & propVal)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
setDbusProperty(const std::shared_ptr<sdbusplus::asio::connection> & conn,size_t cpu,const CpuProperty & newProp)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 */
createCpuUpdatedMatch(const std::shared_ptr<sdbusplus::asio::connection> & conn,size_t cpu)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
getPPIN(boost::asio::io_service & io,const std::shared_ptr<sdbusplus::asio::connection> & conn,const size_t & cpu)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
getCpuAddress(boost::asio::io_service & io,const std::shared_ptr<sdbusplus::asio::connection> & conn,const std::string & service,const std::string & object,const std::string & interface)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 */
getCpuConfiguration(boost::asio::io_service & io,const std::shared_ptr<sdbusplus::asio::connection> & conn,sdbusplus::asio::object_server & objServer)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
main()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