xref: /openbmc/smbios-mdr/src/cpuinfo_main.cpp (revision a30229e1)
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