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