xref: /illumos-gate/usr/src/cmd/nvmeadm/nvmeadm_phyeye.c (revision c96729ea4b2aa7ebf343d6a8572785f8f1caed5f)
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