1 /*
2 * This file and its contents are supplied under the terms of the
3 * Common Development and Distribution License ("CDDL"), version 1.0.
4 * You may only use this file in accordance with the terms of version
5 * 1.0 of the CDDL.
6 *
7 * A full copy of the text of the CDDL should have accompanied this
8 * source. A copy of the CDDL is also available via the Internet at
9 * http://www.illumos.org/license/CDDL.
10 */
11
12 /*
13 * Copyright 2026 Oxide Computer Company
14 */
15
16 /*
17 * Logic to gather the physical interface receiver eye opening measurement and
18 * slice and dice it.
19 */
20
21 #include <err.h>
22 #include <fcntl.h>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <strings.h>
26 #include <unistd.h>
27 #include <sys/stat.h>
28 #include <sys/debug.h>
29 #include <sys/sysmacros.h>
30 #include <sys/mman.h>
31 #include <sys/bitext.h>
32 #include <ofmt.h>
33
34 #include "nvmeadm.h"
35
36 /*
37 * Pick a reasonable buffer size that we think will fit within a device's
38 * maximum payload. For now we use 64 KiB somewhat arbitrarily.
39 */
40 #define PHYEYE_BUFSIZE (64 * 1024)
41
42 /*
43 * Maximum values for PCIe lanes and eyes. We use 16 lanes for PCIe as while
44 * most devices are only x4, that's the largest sized slot that's typically
45 * implemented.
46 *
47 * For the maximum number of eyes there is only 1 for NRZ and today 3 for PAM4.
48 * So we pick the PAM4 default.
49 */
50 #define PHYEYE_MAX_LANE 16
51 #define PHYEYE_MAX_EYE 3
52
53 typedef struct {
54 const char *pm_output;
55 nvme_eom_lsp_mqual_t pm_qual;
56 } phyeye_measure_t;
57
58 typedef enum {
59 PHYEYE_REPORT_M_ASCII,
60 PHYEYE_REPORT_M_OED
61 } phyeye_report_mode_t;
62
63 typedef struct {
64 nvme_process_arg_t *pr_npa;
65 const char *pr_input;
66 phyeye_report_mode_t pr_mode;
67 uint8_t pr_lane;
68 uint8_t pr_eye;
69 bool pr_print;
70 } phyeye_report_t;
71
72 static const nvmeadm_field_bit_t phyeye_eom_odp_bits[] = { {
73 .nfb_lowbit = 0, .nfb_hibit = 0,
74 .nfb_short = "pefp",
75 .nfb_desc = "Printable Eye Field Present",
76 .nfb_type = NVMEADM_FT_STRMAP,
77 .nfb_strs = { "not present", "present" }
78 }, {
79 .nfb_lowbit = 1, .nfb_hibit = 1,
80 .nfb_short = "edfp",
81 .nfb_desc = "Eye Data Field Present",
82 .nfb_type = NVMEADM_FT_STRMAP,
83 .nfb_strs = { "not present", "present" }
84 } };
85
86 static const nvmeadm_field_bit_t phyeye_eom_lspfc_bits[] = { {
87 .nfb_lowbit = 0, .nfb_hibit = 6,
88 .nfb_short = "lspfv",
89 .nfb_desc = "Log Specific Parameter Field Value",
90 .nfb_type = NVMEADM_FT_HEX
91 } };
92
93 static const nvmeadm_field_bit_t phyeye_eom_linfo_bits[] = { {
94 .nfb_lowbit = 0, .nfb_hibit = 3,
95 .nfb_short = "mls",
96 .nfb_desc = "Measurement Link Speed",
97 .nfb_type = NVMEADM_FT_HEX
98 } };
99
100 #define PHYEYE_F_EOM(f) .nf_off = offsetof(nvme_eom_hdr_t, eom_##f), \
101 .nf_len = sizeof (((nvme_eom_hdr_t *)NULL)->eom_##f), \
102 .nf_short = #f
103
104 static const nvmeadm_field_t phyeye_eom_fields[] = { {
105 PHYEYE_F_EOM(lid),
106 .nf_desc = "Log Identifier",
107 .nf_type = NVMEADM_FT_HEX
108 }, {
109 PHYEYE_F_EOM(eomip),
110 .nf_desc = "EOM In Progress",
111 .nf_type = NVMEADM_FT_STRMAP,
112 .nf_strs = { "no measurement", "in progress", "completed" },
113 }, {
114 PHYEYE_F_EOM(hsize),
115 .nf_desc = "Header Size",
116 .nf_type = NVMEADM_FT_HEX
117 }, {
118 PHYEYE_F_EOM(rsz),
119 .nf_desc = "Result Size",
120 .nf_type = NVMEADM_FT_HEX
121 }, {
122 PHYEYE_F_EOM(edgn),
123 .nf_desc = "EOM Data Generation Number",
124 .nf_type = NVMEADM_FT_HEX
125 }, {
126 PHYEYE_F_EOM(lrev),
127 .nf_desc = "Log Revision",
128 .nf_type = NVMEADM_FT_HEX
129 }, {
130 PHYEYE_F_EOM(odp),
131 .nf_desc = "Optional Data Present",
132 NVMEADM_F_BITS(phyeye_eom_odp_bits)
133 }, {
134 PHYEYE_F_EOM(lns),
135 .nf_desc = "Lanes",
136 .nf_type = NVMEADM_FT_HEX
137 }, {
138 PHYEYE_F_EOM(epl),
139 .nf_desc = "Eyes Per Lane",
140 .nf_type = NVMEADM_FT_HEX
141 }, {
142 PHYEYE_F_EOM(lspfc),
143 .nf_desc = "Log Specific Parameter Field Copy",
144 NVMEADM_F_BITS(phyeye_eom_lspfc_bits)
145 }, {
146 PHYEYE_F_EOM(linfo),
147 .nf_desc = "Link Information",
148 NVMEADM_F_BITS(phyeye_eom_linfo_bits)
149 }, {
150 PHYEYE_F_EOM(lsic),
151 .nf_desc = "Log Specific Identifier Copy",
152 .nf_type = NVMEADM_FT_HEX
153 }, {
154 PHYEYE_F_EOM(ds),
155 .nf_desc = "Descriptor Size",
156 .nf_type = NVMEADM_FT_HEX
157 }, {
158 PHYEYE_F_EOM(nd),
159 .nf_desc = "Number of Descriptors",
160 .nf_type = NVMEADM_FT_HEX
161 }, {
162 PHYEYE_F_EOM(maxtb),
163 .nf_desc = "Maximum Top Bottom",
164 .nf_type = NVMEADM_FT_HEX
165 }, {
166 PHYEYE_F_EOM(maxlr),
167 .nf_desc = "Maximum Left Right",
168 .nf_type = NVMEADM_FT_HEX
169 }, {
170 PHYEYE_F_EOM(etgood),
171 .nf_desc = "Estimated Time for Good Quality",
172 .nf_type = NVMEADM_FT_HEX
173 }, {
174 PHYEYE_F_EOM(etbetter),
175 .nf_desc = "Estimated Time for Better Quality",
176 .nf_type = NVMEADM_FT_HEX
177 }, {
178 PHYEYE_F_EOM(etbest),
179 .nf_desc = "Estimated Time for Best Quality",
180 .nf_type = NVMEADM_FT_HEX
181 } };
182
183 static const nvmeadm_field_bit_t phyeye_eld_mstat_bits[] = { {
184 .nfb_lowbit = 0, .nfb_hibit = 0,
185 .nfb_short = "mscs",
186 .nfb_desc = "Measurement Successful",
187 .nfb_type = NVMEADM_FT_STRMAP,
188 .nfb_strs = { "no", "yes" }
189 } };
190
191 #define PHYEYE_F_ELD(f) .nf_off = offsetof(nvme_eom_lane_desc_t, eld_##f), \
192 .nf_len = sizeof (((nvme_eom_lane_desc_t *)NULL)->eld_##f), \
193 .nf_short = #f
194
195 static const nvmeadm_field_t phyeye_elm_fields[] = { {
196 PHYEYE_F_ELD(mstat),
197 .nf_desc = "Measurement Status",
198 NVMEADM_F_BITS(phyeye_eld_mstat_bits)
199 }, {
200 PHYEYE_F_ELD(ln),
201 .nf_desc = "Lane",
202 .nf_type = NVMEADM_FT_HEX
203 }, {
204 PHYEYE_F_ELD(eye),
205 .nf_desc = "Eye",
206 .nf_type = NVMEADM_FT_HEX
207 }, {
208 PHYEYE_F_ELD(top),
209 .nf_desc = "Top",
210 .nf_type = NVMEADM_FT_HEX
211 }, {
212 PHYEYE_F_ELD(btm),
213 .nf_desc = "Bottom",
214 .nf_type = NVMEADM_FT_HEX
215 }, {
216 PHYEYE_F_ELD(lft),
217 .nf_desc = "Left",
218 .nf_type = NVMEADM_FT_HEX
219 }, {
220 PHYEYE_F_ELD(rgt),
221 .nf_desc = "Right",
222 .nf_type = NVMEADM_FT_HEX
223 }, {
224 PHYEYE_F_ELD(nrows),
225 .nf_desc = "Number of Rows",
226 .nf_type = NVMEADM_FT_HEX
227 }, {
228 PHYEYE_F_ELD(ncols),
229 .nf_desc = "Number of Columns",
230 .nf_type = NVMEADM_FT_HEX
231 }, {
232 /*
233 * The size of this field changed between revision 2 and 3 from 2 bytes
234 * to 4 bytes.
235 */
236 .nf_off = offsetof(nvme_eom_lane_desc_t, eld_edlen),
237 .nf_len = sizeof (uint16_t),
238 .nf_short = "edlen",
239 .nf_desc = "Eye Data Length",
240 .nf_type = NVMEADM_FT_HEX,
241 .nf_rev = 2
242 }, {
243 PHYEYE_F_ELD(edlen),
244 .nf_desc = "Eye Data Length",
245 .nf_type = NVMEADM_FT_HEX,
246 .nf_rev = 3
247 } };
248
249 void
usage_measure_phyeye_cmd(const char * c_name)250 usage_measure_phyeye_cmd(const char *c_name)
251 {
252 (void) fprintf(stderr, "%s -o output [-Q good | better | best] "
253 "<ctl>\n\n", c_name);
254 (void) fprintf(stderr, " Gather physical eye opening measurements "
255 "from the named controller and save\n them to the specified "
256 "output file. The best quality measurement is taken by\n "
257 "default. No other administrative operations can be executed "
258 "during the eye\n measurement.\n");
259 }
260
261 void
optparse_measure_phyeye_cmd(nvme_process_arg_t * npa)262 optparse_measure_phyeye_cmd(nvme_process_arg_t *npa)
263 {
264 int c;
265 phyeye_measure_t *phy;
266
267 if ((phy = calloc(1, sizeof (phyeye_measure_t))) == NULL) {
268 err(-1, "failed to allocate memory for option tracking");
269 }
270
271 /*
272 * Default to best quality if not otherwise requested.
273 */
274 phy->pm_qual = NVME_EOM_LSP_MQUAL_BEST;
275
276 while ((c = getopt(npa->npa_argc, npa->npa_argv, ":o:Q:")) != -1) {
277 switch (c) {
278 case 'o':
279 phy->pm_output = optarg;
280 break;
281 case 'Q':
282 if (strcasecmp(optarg, "good") == 0) {
283 phy->pm_qual = NVME_EOM_LSP_MQUAL_GOOD;
284 } else if (strcasecmp(optarg, "better") == 0) {
285 phy->pm_qual = NVME_EOM_LSP_MQUAL_BETTER;
286
287 } else if (strcasecmp(optarg, "best") == 0) {
288 phy->pm_qual = NVME_EOM_LSP_MQUAL_BEST;
289
290 } else {
291 errx(-1, "invalid quality value %s: valid "
292 "values are 'good', 'better', or 'best'",
293 optarg);
294 }
295 break;
296 case '?':
297 errx(-1, "unknown option: -%c", optopt);
298 case ':':
299 errx(-1, "option -%c requires an argument", optopt);
300 }
301 }
302
303 if (phy->pm_output == NULL) {
304 errx(-1, "missing required output file (-o)");
305 }
306
307 npa->npa_cmd_arg = phy;
308 }
309
310 /*
311 * Poll the log page until it is done and ready. We always do the initial wait.
312 * The specification says that this may take longer due to activity on the
313 * device. We will wait up to 3x the amount of time that was indicated for this
314 * measurement. We will begin using a 1 second delay after this point.
315 *
316 * This explicitly uses a volatile pointer for 'eom' due to the fact that the
317 * log page execution will update the data that it points to.
318 */
319 static void
nvmeadm_phyeye_wait(const nvme_process_arg_t * npa,nvme_log_req_t * req,volatile nvme_eom_hdr_t * eom,uint16_t wait,const char * qual)320 nvmeadm_phyeye_wait(const nvme_process_arg_t *npa, nvme_log_req_t *req,
321 volatile nvme_eom_hdr_t *eom, uint16_t wait, const char *qual)
322 {
323 hrtime_t start = gethrtime();
324 hrtime_t end = start + wait * NANOSEC;
325 hrtime_t now = start;
326 hrtime_t max = start + wait * 3 * NANOSEC;
327 const bool tty = isatty(STDOUT_FILENO);
328
329 (void) printf("device indicates a minimum %u second wait for %s "
330 "quality phyeye measurement\n", wait, qual);
331 while (now < end) {
332 if (tty) {
333 (void) printf("\r%u/%u seconds elapsed",
334 (now - start) / NANOSEC, wait);
335 (void) fflush(stdout);
336 }
337 (void) sleep(1);
338 now = gethrtime();
339 }
340
341 if (tty) {
342 (void) printf("\r%u/%u seconds elapsed\n", wait, wait);
343 }
344
345 if (!nvme_log_req_exec(req)) {
346 nvmeadm_fatal(npa, "failed to retrieve phyeye measurement log "
347 "request");
348 }
349
350 if (eom->eom_eomip == NVME_EOM_DONE) {
351 return;
352 }
353
354 (void) printf("Measurement incomplete, proceeding to check over an "
355 "additional %u seconds\n", max - wait);
356 uint32_t extra = 0;
357 now = gethrtime();
358 hrtime_t phase2 = now;
359 while (now < max) {
360 (void) sleep(1);
361
362 if (!nvme_log_req_exec(req)) {
363 nvmeadm_fatal(npa, "failed to issue start phyeye "
364 "measurement log request");
365 }
366
367 now = gethrtime();
368
369 if (eom->eom_eomip == NVME_EOM_DONE) {
370 return;
371 }
372
373 extra++;
374 if (tty) {
375 (void) printf("\rMeasurement still not available after "
376 "%u attempts (%u seconds)", extra, (now - phase2) /
377 NANOSEC);
378 (void) fflush(stdout);
379 }
380 }
381
382 errx(-1, "timed out waiting for the phyeye measurement to finish after "
383 "%u seconds: final measurement state: %u", wait * 3,
384 eom->eom_eomip);
385 }
386
387 static void
nvmeadm_phyeye_read(const nvme_process_arg_t * npa,nvme_log_req_t * req,void * buf,size_t len,uint64_t off)388 nvmeadm_phyeye_read(const nvme_process_arg_t *npa, nvme_log_req_t *req,
389 void *buf, size_t len, uint64_t off)
390 {
391 if (!nvme_log_req_set_output(req, buf, len)) {
392 nvmeadm_fatal(npa, "failed to set output buffer");
393 }
394
395 if (!nvme_log_req_set_offset(req, off)) {
396 nvmeadm_fatal(npa, "failed to set offset to 0x%lx", off);
397 }
398
399 if (!nvme_log_req_exec(req)) {
400 nvmeadm_fatal(npa, "failed to read %zu bytes at 0x%lx", len,
401 off);
402 }
403 }
404
405 static void
nvmeadm_phyeye_write(int fd,const void * buf,size_t len,off_t off)406 nvmeadm_phyeye_write(int fd, const void *buf, size_t len, off_t off)
407 {
408 size_t loff = 0;
409
410 while (len > 0) {
411 ssize_t ret = pwrite(fd, buf + loff, len, off + loff);
412 if (ret < 0) {
413 err(EXIT_FAILURE, "failed to write to physical eye "
414 "measurement output file");
415 }
416
417 loff += (size_t)ret;
418 len -= (size_t)ret;
419 }
420 }
421
422 /*
423 * Perform a physical eye measurement. This consists of a few different steps to
424 * execute it successfully:
425 *
426 * 1. First determine that we can actually issue this command.
427 * 2. Open the output file early. While this may mean we truncate something,
428 * given that this command may take some time, that's better than finding out
429 * after you've already done all the work.
430 * 3. We issue the first phy eye get log page command with the request to begin
431 * a new measurement at the requested quality. We need to set the LSP, LSI,
432 * and output buffer for this.
433 * 4. We wait for the requested number of seconds before beginning to query for
434 * result data.
435 * 5. Once a second, we issue commands trying to see if it's done.
436 * 6. Once it's finally done, then we'll go ahead and actually finish getting
437 * the log page data and write it out to disk.
438 * 7. When we're done with all the data, confirm that the generation is still
439 * the same as when we started.
440 */
441 int
do_measure_phyeye_cmd(const nvme_process_arg_t * npa)442 do_measure_phyeye_cmd(const nvme_process_arg_t *npa)
443 {
444 int fd = -1;
445 nvme_log_req_t *req = NULL;
446 nvme_log_disc_t *disc = NULL;
447 nvme_eom_lsp_t lsp;
448 const phyeye_measure_t *phy = npa->npa_cmd_arg;
449 void *buf = NULL;
450 uint64_t min_len;
451 nvme_log_size_kind_t lkind;
452
453 if (!nvme_log_req_init_by_name(npa->npa_ctrl, "phyeye", 0, &disc,
454 &req)) {
455 nvmeadm_fatal(npa, "failed to initialize phyeye log request");
456 }
457
458 if ((fd = open(phy->pm_output, O_RDWR | O_CREAT | O_TRUNC, 0644)) < 0) {
459 err(-1, "failed to open output file %s", phy->pm_output);
460 }
461
462 if ((buf = malloc(PHYEYE_BUFSIZE)) == NULL) {
463 err(-1, "failed to allocate internal phy data buffer");
464 }
465
466 if (!nvme_log_req_set_lsi(req, npa->npa_idctl->id_cntlid)) {
467 nvmeadm_fatal(npa, "failed to set lsi for phyeye measurement");
468 }
469
470 (void) memset(&lsp, 0, sizeof (lsp));
471 lsp.nel_mqual = phy->pm_qual;
472 lsp.nel_act = NVME_EOM_LSP_START;
473
474 if (!nvme_log_req_set_lsp(req, lsp.r)) {
475 nvmeadm_fatal(npa, "failed to set lsp for phyeye measurement");
476 }
477
478 lkind = nvme_log_disc_size(disc, &min_len);
479 VERIFY3U(lkind, ==, NVME_LOG_SIZE_K_VAR);
480 VERIFY3U(min_len, >=, sizeof (nvme_eom_hdr_t));
481
482 if (!nvme_log_req_set_output(req, buf, min_len)) {
483 nvmeadm_fatal(npa, "failed to set initial output buffer and "
484 "length");
485 }
486
487 if (!nvme_log_req_exec(req)) {
488 nvmeadm_fatal(npa, "failed to issue start phyeye measurement "
489 "log request");
490 }
491
492 /*
493 * Update the request for the rest of this to always be a read request
494 * of the existing measurement.
495 */
496 lsp.nel_act = NVME_EOM_LSP_READ;
497 if (!nvme_log_req_set_lsp(req, lsp.r)) {
498 nvmeadm_fatal(npa, "failed to update lsp for phyeye "
499 "measurement");
500 }
501
502 /*
503 * The use of volatile here is probably a little weird. But this is
504 * aliasing memory that the log req exec will constantly be updating.
505 */
506 const volatile nvme_eom_hdr_t *eom = buf;
507 if (eom->eom_eomip != NVME_EOM_IN_PROGRESS) {
508 warnx("EOM in progress in header is not in-progress, found %u: "
509 "waiting the appropriate time regardless", eom->eom_eomip);
510 }
511
512 const uint8_t eom_gen = eom->eom_edgn;
513
514 uint16_t wait;
515 const char *desc;
516 if (phy->pm_qual == NVME_EOM_LSP_MQUAL_GOOD) {
517 wait = eom->eom_etgood;
518 desc = "good";
519 } else if (phy->pm_qual == NVME_EOM_LSP_MQUAL_BETTER) {
520 wait = eom->eom_etbetter;
521 desc = "better";
522 } else {
523 wait = eom->eom_etbest;
524 desc = "best";
525 }
526 nvmeadm_phyeye_wait(npa, req, buf, wait, desc);
527
528 /*
529 * Go ahead and calculate the final size. At this point we'll issue
530 * requests that adjust the overall offset until we read everything and
531 * write that out.
532 */
533 uint64_t act_len, off = 0;
534 if (!nvme_log_disc_calc_size(disc, &act_len, buf, min_len)) {
535 errx(-1, "failed to determine full phyeye log length");
536 }
537
538 while (off < act_len) {
539 size_t to_read = MIN(act_len - off, PHYEYE_BUFSIZE);
540 nvmeadm_phyeye_read(npa, req, buf, to_read, off);
541 nvmeadm_phyeye_write(fd, buf, to_read, off);
542 off += to_read;
543 }
544
545 /*
546 * Now that we're done, get the initial header's worth of data again and
547 * verify its generation to make sure nothing has changed on us.
548 */
549 nvmeadm_phyeye_read(npa, req, buf, sizeof (nvme_eom_hdr_t), 0);
550 if (eom->eom_edgn != eom_gen) {
551 (void) unlink(phy->pm_output);
552 errx(-1, "PHY eye measurement generation unexpectedly changed: "
553 "was 0x%x, now is 0x%x: aborting", eom_gen, eom->eom_edgn);
554 }
555
556 /*
557 * Note we don't actually clear the data here and basically are willing
558 * to leave this in the controller at this point.
559 */
560 (void) printf("phyeye successfully written to %s\n", phy->pm_output);
561
562 free(buf);
563 if (fd >= 0) {
564 VERIFY0(close(fd));
565 }
566 nvme_log_disc_free(disc);
567 nvme_log_req_fini(req);
568 return (0);
569 }
570
571 void
usage_report_phyeye_cmd(const char * c_name)572 usage_report_phyeye_cmd(const char *c_name)
573 {
574 (void) fprintf(stderr, "%s -f file [-l lane] [-e eye] [-m mode] "
575 "<ctl>\n\n", c_name);
576 (void) fprintf(stderr, " Report information about a physical eye "
577 "measurement. Eye measurements can be\n taken with \"nvmeadm "
578 "measure-phyeye\".\n");
579 }
580
581 void
optparse_report_phyeye_cmd(nvme_process_arg_t * npa)582 optparse_report_phyeye_cmd(nvme_process_arg_t *npa)
583 {
584 int c;
585 phyeye_report_t *phy;
586
587 if ((phy = calloc(1, sizeof (phyeye_report_t))) == NULL) {
588 err(-1, "failed to allocate memory for option tracking");
589 }
590
591 phy->pr_npa = npa;
592 phy->pr_mode = PHYEYE_REPORT_M_ASCII;
593 phy->pr_lane = UINT8_MAX;
594 phy->pr_eye = UINT8_MAX;
595
596 while ((c = getopt(npa->npa_argc, npa->npa_argv, ":e:f:l:m:")) != -1) {
597 const char *errstr;
598
599 switch (c) {
600 case 'e':
601 phy->pr_eye = strtonumx(optarg, 0, PHYEYE_MAX_EYE,
602 &errstr, 0);
603 if (errstr != NULL) {
604 errx(-1, "failed to parse eye: value %s is "
605 "%s: valid values are in the range [%u, "
606 "%u]", optarg, errstr, 0, PHYEYE_MAX_EYE);
607 }
608 break;
609 case 'f':
610 phy->pr_input = optarg;
611 break;
612 case 'l':
613 phy->pr_lane = strtonumx(optarg, 0, PHYEYE_MAX_LANE,
614 &errstr, 0);
615 if (errstr != NULL) {
616 errx(-1, "failed to parse lane: value %s is "
617 "%s: valid values are in the range [%u, "
618 "%u]", optarg, errstr, 0, PHYEYE_MAX_LANE);
619 }
620 break;
621 case 'm':
622 if (strcasecmp(optarg, "print-eye") == 0) {
623 phy->pr_mode = PHYEYE_REPORT_M_ASCII;
624 } else if (strcasecmp(optarg, "eye-data") == 0) {
625 phy->pr_mode = PHYEYE_REPORT_M_OED;
626 } else {
627 errx(-1, "invalid mode value: %s: valid values "
628 "are 'print-eye' or 'eye-data'", optarg);
629 }
630 break;
631 case '?':
632 errx(-1, "unknown option: -%c", optopt);
633 case ':':
634 errx(-1, "option -%c requires an argument", optopt);
635
636 }
637 }
638
639 if (phy->pr_input == NULL) {
640 errx(-1, "missing required input file to process (-f)");
641 }
642
643 npa->npa_cmd_arg = phy;
644 }
645
646 /*
647 * Normalize the optional eye data length. This is a uint32_t in revision 3
648 * logs, but a uint16_t in revision 2 logs.
649 */
650 static uint32_t
phyeye_eol_oed_len(const nvme_eom_hdr_t * hdr,const nvme_eom_lane_desc_t * desc)651 phyeye_eol_oed_len(const nvme_eom_hdr_t *hdr, const nvme_eom_lane_desc_t *desc)
652 {
653 uint16_t u16;
654
655 if (hdr->eom_lrev >= 3) {
656 return (desc->eld_edlen);
657 }
658
659 (void) memcpy(&u16, &desc->eld_edlen, sizeof (uint16_t));
660 return (u16);
661 }
662
663 typedef void (*phyeye_lane_iter_cb_f)(uint8_t, const nvme_eom_hdr_t *,
664 const nvme_eom_lane_desc_t *, void *);
665
666 static bool
phyeye_lane_iter(const nvme_eom_hdr_t * hdr,off_t max,void * arg,phyeye_lane_iter_cb_f func,uint8_t lane,uint8_t eye)667 phyeye_lane_iter(const nvme_eom_hdr_t *hdr, off_t max,
668 void *arg, phyeye_lane_iter_cb_f func, uint8_t lane, uint8_t eye)
669 {
670 off_t cur_off = sizeof (nvme_eom_hdr_t);
671 bool ret = true;
672
673 for (uint16_t i = 0; i < hdr->eom_nd; i++) {
674 const nvme_eom_lane_desc_t *desc = NULL;
675 size_t dlen = sizeof (nvme_eom_lane_desc_t);
676
677 if (cur_off + hdr->eom_ds > max) {
678 errx(-1, "failed to iterate EOM Lane descriptors: "
679 "descriptor %u starts at offset 0x%lx, but its "
680 "size (0x%x) would exceed the maximum file length "
681 "of 0x%lx", i, cur_off, hdr->eom_ds, max);
682 }
683
684 desc = (const nvme_eom_lane_desc_t *)((uintptr_t)hdr + cur_off);
685
686 if (hdr->eom_odp.odp_pefp != 0) {
687 if (desc->eld_nrows == 0 || desc->eld_ncols == 0) {
688 errx(-1, "printable eye feature present but "
689 "both NROWS (0x%x) and NCOLS (0x%x) are "
690 "not non-zero", desc->eld_nrows,
691 desc->eld_ncols);
692 }
693
694 dlen += desc->eld_nrows * desc->eld_ncols;
695 } else if (desc->eld_nrows != 0 || desc->eld_ncols != 0) {
696 errx(-1, "printable eye feature not present but both "
697 "NROWS (0x%x) and NCOLS (0x%x) are not zero",
698 desc->eld_nrows, desc->eld_ncols);
699 }
700
701 const uint32_t oed_len = phyeye_eol_oed_len(hdr, desc);
702 if (hdr->eom_odp.odp_edfp != 0) {
703 if (oed_len == 0) {
704 errx(-1, "optional eye data feature present, "
705 "but eye data has a zero-length");
706 }
707
708 dlen += oed_len;
709 } else if (oed_len != 0) {
710 errx(-1, "optional eye data feature not present, but "
711 "eye data has a non-zero length (0x%x)", oed_len);
712 }
713
714 if (dlen > hdr->eom_ds) {
715 errx(-1, "failed to iterate EOM Lane descriptors: "
716 "descriptor %u starts at offset 0x%lx, has a "
717 "calculated size (0x%zx) that exceeds the "
718 "header's max descriptor size (0x%x)", i, cur_off,
719 dlen, hdr->eom_ds);
720 }
721
722 /*
723 * Now that we've validated this we need to check a few things
724 * before we call the command:
725 *
726 * 1. This matches our eye and lane filter.
727 * 2. The data is valid.
728 */
729 if (lane != UINT8_MAX && desc->eld_ln != lane)
730 goto next;
731 if (eye != UINT8_MAX && desc->eld_eye != eye)
732 goto next;
733 if (desc->eld_mstat.mstat_mcsc == 0) {
734 warnx("lane %u, eye %u data does not have a successful "
735 "measurement", desc->eld_ln, desc->eld_eye);
736 ret = false;
737 goto next;
738 }
739
740 func(i, hdr, desc, arg);
741
742 next:
743 cur_off += dlen;
744 }
745
746 return (ret);
747 }
748
749 /*
750 * Validate the data that we have. In particular we need to confirm:
751 *
752 * 1. The data file covers the entire header.
753 * 2. This is a log revision we know about.
754 * 3. The measurement is completed.
755 * 4. The header size reported is what we expect.
756 * 5. The result size is covered by the file.
757 * 6. If a specific mode requires optional data, it is present.
758 * 7. There is a non-zero number of descriptors.
759 * 8. The descriptor size covers at least the Lane descriptor structure.
760 * 9. DS * NDS is within the result size.
761 *
762 * The specifics of each descriptor are checked when we iterate over them in
763 * phyeye_eol_iter().
764 */
765 static void
phyeye_report_sanity_check(const nvme_eom_hdr_t * hdr,off_t len,const phyeye_report_t * phy)766 phyeye_report_sanity_check(const nvme_eom_hdr_t *hdr, off_t len,
767 const phyeye_report_t *phy)
768 {
769 if (len < sizeof (nvme_eom_hdr_t)) {
770 errx(-1, "data file is too short: file does not cover the "
771 "0x%lx bytes required for the Eye Opening Measurement "
772 "header", sizeof (nvme_eom_hdr_t));
773 }
774
775 /*
776 * This specification was first introduced in the NVMe PCIe revision 1.1
777 * at log revision 2. It moved to version 3 with NVMe PCIe 1.2. However,
778 * some devices report log revision 1 which means they likely implement
779 * a draft version of the TP. We don't know what's different between
780 * version 1 and 2, but hope for the sake of understanding right now
781 * it doesn't impact our ability to translate this.
782 */
783 if (hdr->eom_lrev < 1 || hdr->eom_lrev > 3) {
784 errx(-1, "encountered unknown log header revision: 0x%x",
785 hdr->eom_lrev);
786 }
787
788 /*
789 * Only worry about complete measurements if we're doing a report, not
790 * if we're just printing the log.
791 */
792 if (phy != NULL && hdr->eom_eomip != NVME_EOM_DONE) {
793 errx(-1, "data file measurement in progress field does not "
794 "indicate a finished measurement (%u): found %u",
795 NVME_EOM_DONE, hdr->eom_eomip);
796 }
797
798 if (hdr->eom_hsize != sizeof (nvme_eom_hdr_t)) {
799 errx(-1, "data file has unexpected header length: found 0x%x, "
800 "expected 0x%zx", hdr->eom_hsize, sizeof (nvme_eom_hdr_t));
801 }
802
803 if (hdr->eom_rsz > len) {
804 errx(-1, "data file reports that the log is 0x%x bytes, but "
805 "file is only 0x%lx bytes", hdr->eom_rsz, len);
806 }
807
808 if (phy != NULL && phy->pr_mode == PHYEYE_REPORT_M_ASCII &&
809 hdr->eom_odp.odp_pefp == 0) {
810 errx(-1, "Printable Eye requested, but field not present in "
811 "data file");
812 }
813
814 if (phy != NULL && phy->pr_mode == PHYEYE_REPORT_M_OED &&
815 hdr->eom_odp.odp_edfp == 0) {
816 errx(-1, "Eye Data Field requested, but field not present in "
817 "data file");
818 }
819
820 if (phy != NULL && hdr->eom_nd == 0) {
821 errx(-1, "data file reports no EOM lane descriptors present");
822 }
823
824 if (hdr->eom_nd > 0 && hdr->eom_ds < sizeof (nvme_eom_lane_desc_t)) {
825 errx(-1, "data file reports the descriptor size is 0x%x, but "
826 "that is less than the base descriptor size of 0x%zx",
827 hdr->eom_ds, sizeof (nvme_eom_lane_desc_t));
828 }
829 }
830
831 static void
phyeye_report_ascii(uint8_t descno,const nvme_eom_hdr_t * hdr,const nvme_eom_lane_desc_t * desc,void * arg)832 phyeye_report_ascii(uint8_t descno, const nvme_eom_hdr_t *hdr,
833 const nvme_eom_lane_desc_t *desc, void *arg)
834 {
835 phyeye_report_t *phy = arg;
836
837 phy->pr_print = true;
838 (void) printf("Lane %u, Eye %u: Printable Eye\n", desc->eld_ln,
839 desc->eld_eye);
840 for (uint16_t row = 0; row < desc->eld_nrows; row++) {
841 for (uint16_t col = 0; col < desc->eld_ncols; col++) {
842 const uint32_t off = row * desc->eld_ncols + col;
843 uint8_t c = desc->eld_data[off];
844 if (c != '0' && c != '1')
845 c = '?';
846 (void) putc(c, stdout);
847 }
848 (void) putc('\n', stdout);
849 }
850 }
851
852 static void
phyeye_report_oed(uint8_t descno,const nvme_eom_hdr_t * hdr,const nvme_eom_lane_desc_t * desc,void * arg)853 phyeye_report_oed(uint8_t descno, const nvme_eom_hdr_t *hdr,
854 const nvme_eom_lane_desc_t *desc, void *arg)
855 {
856 phyeye_report_t *phy = arg;
857
858 size_t off = desc->eld_nrows * desc->eld_ncols;
859
860 phy->pr_print = true;
861 (void) printf("Lane %u, Eye %u: Eye Data\n", desc->eld_ln,
862 desc->eld_eye);
863 nvmeadm_dump_hex(&desc->eld_data[off], desc->eld_edlen);
864 }
865
866 int
do_report_phyeye_cmd(const nvme_process_arg_t * npa)867 do_report_phyeye_cmd(const nvme_process_arg_t *npa)
868 {
869 int fd = -1, ret = EXIT_SUCCESS;
870 struct stat st;
871 void *data;
872 phyeye_report_t *phy = npa->npa_cmd_arg;
873 phyeye_lane_iter_cb_f func = NULL;
874
875 if (npa->npa_argc > 0) {
876 errx(-1, "extraneous arguments beginning with '%s'",
877 npa->npa_argv[0]);
878 }
879
880 if ((fd = open(phy->pr_input, O_RDONLY)) < 0) {
881 err(-1, "failed to open input file %s", phy->pr_input);
882 }
883
884 if (fstat(fd, &st) != 0) {
885 err(-1, "failed to stat %s", phy->pr_input);
886 }
887
888 if (st.st_size > NVMEADM_MAX_MMAP) {
889 errx(-1, "%s file size of 0x%lx exceeds maximum allowed size "
890 "of 0x%llx", phy->pr_input, st.st_size, NVMEADM_MAX_MMAP);
891 }
892
893 data = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
894 if (data == MAP_FAILED) {
895 errx(-1, "failed to mmap %s", phy->pr_input);
896 }
897
898 phyeye_report_sanity_check(data, st.st_size, phy);
899
900 switch (phy->pr_mode) {
901 case PHYEYE_REPORT_M_ASCII:
902 func = phyeye_report_ascii;
903 break;
904 case PHYEYE_REPORT_M_OED:
905 func = phyeye_report_oed;
906 break;
907 }
908
909 if (!phyeye_lane_iter(data, st.st_size, phy, func, phy->pr_lane,
910 phy->pr_eye)) {
911 ret = -1;
912 }
913
914 /*
915 * If nothing was printed, warn and error about this if we had an eye
916 * constraint, a lane constraint, or the mode isn't decoding. We want to
917 * be able to decode a log that has no data.
918 */
919 if (!phy->pr_print) {
920 warnx("failed to match and print any data");
921 ret = -1;
922 }
923
924 VERIFY0(munmap(data, st.st_size));
925 if (fd >= 0) {
926 VERIFY0(close(fd));
927 }
928
929 return (ret);
930 }
931
932 static uint32_t
phyeye_log_getrev(const void * data,size_t len)933 phyeye_log_getrev(const void *data, size_t len)
934 {
935 const nvme_eom_hdr_t *hdr = data;
936 return (hdr->eom_lrev);
937 }
938
939 static void
phyeye_log_drive_lane_cb(uint8_t descno,const nvme_eom_hdr_t * hdr,const nvme_eom_lane_desc_t * desc,void * arg)940 phyeye_log_drive_lane_cb(uint8_t descno, const nvme_eom_hdr_t *hdr,
941 const nvme_eom_lane_desc_t *desc, void *arg)
942 {
943 char base[32];
944 char header[128];
945 nvmeadm_field_print_t *print = arg;
946
947 (void) snprintf(base, sizeof (base), "eld%u", descno);
948 (void) snprintf(header, sizeof (header), "EOM Lane Descriptor %u",
949 descno);
950
951 print->fp_header = header;
952 print->fp_fields = phyeye_elm_fields;
953 print->fp_nfields = ARRAY_SIZE(phyeye_elm_fields);
954 print->fp_base = base;
955 print->fp_data = desc;
956 print->fp_dlen = hdr->eom_ds;
957 print->fp_off = (uintptr_t)desc - (uintptr_t)hdr;
958
959 nvmeadm_field_print(print);
960 }
961
962 static bool
phyeye_log_drive(nvmeadm_field_print_t * print,const void * data,size_t len)963 phyeye_log_drive(nvmeadm_field_print_t *print, const void *data, size_t len)
964 {
965 print->fp_header = "EOM Header";
966 print->fp_fields = phyeye_eom_fields;
967 print->fp_nfields = ARRAY_SIZE(phyeye_eom_fields);
968 print->fp_base = "eom";
969 print->fp_data = data;
970 print->fp_dlen = len;
971 print->fp_off = 0;
972
973 phyeye_report_sanity_check(data, len, NULL);
974 nvmeadm_field_print(print);
975
976 return (phyeye_lane_iter(data, len, print, phyeye_log_drive_lane_cb,
977 UINT8_MAX, UINT8_MAX));
978 }
979
980 const nvmeadm_log_field_info_t phyeye_field_info = {
981 .nlfi_log = "phyeye",
982 .nlfi_min = sizeof (nvme_eom_hdr_t),
983 .nlfi_getrev = phyeye_log_getrev,
984 .nlfi_drive = phyeye_log_drive
985 };
986