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 2025 Oxide Computer Company
14 */
15
16 /*
17 * i2cadm scan -- scan a single port for devices. By default all devices are
18 * scanned under the port, unless a specific set of devices is specified.
19 */
20
21 #include <err.h>
22 #include <stdio.h>
23 #include <stdarg.h>
24 #include <string.h>
25 #include <sys/sysmacros.h>
26 #include <ofmt.h>
27
28 #include "i2cadm.h"
29
30 void
i2cadm_scan_usage(FILE * f)31 i2cadm_scan_usage(FILE *f)
32 {
33 (void) fprintf(f, "\ti2cadm scan [-d dev] [-o field,[...] [-H] [-p]] "
34 "<port>\n");
35 }
36
37 static void
i2cadm_scan_help(const char * fmt,...)38 i2cadm_scan_help(const char *fmt, ...)
39 {
40 if (fmt != NULL) {
41 va_list ap;
42
43 va_start(ap, fmt);
44 vwarnx(fmt, ap);
45 va_end(ap);
46 }
47
48 (void) fprintf(stderr, "Usage: i2cadm scan [-d dev] [-o field,[...] "
49 "[-H] [-p]] <port>\n");
50 (void) fprintf(stderr, "\nScan for I2C devices\n\n"
51 "\t-d dev\t\tonly scan device address dev, can be specified "
52 "multiple\n\t\t\ttimes\n"
53 "\t-H\t\tomit the column header (requires -o)\n"
54 "\t-o field\toutput fields to print\n"
55 "\t-p\t\tparseable output (requires -o)\n");
56 (void) fprintf(stderr, "\nThe following fields are supported when "
57 "using -o:\n"
58 "\taddr\t\tthe I2C address\n"
59 "\tresult\t\tthe address scan result\n"
60 "\terror\t\tthe error message if an error occurred\n");
61 }
62
63 typedef enum {
64 I2CADM_BUS_SCAN_UNKNOWN = 0,
65 I2CADM_BUS_SCAN_FOUND,
66 I2CADM_BUS_SCAN_NO_DEV,
67 I2CADM_BUS_SCAN_RESERVED,
68 I2CADM_BUS_SCAN_TIMEOUT,
69 I2CADM_BUS_SCAN_ERROR,
70 I2CADM_BUS_SCAN_SKIPPED
71 } i2cadm_scan_result_t;
72
73 typedef struct {
74 i2c_addr_t scan_addr;
75 i2cadm_scan_result_t scan_res;
76 char *scan_error;
77 } i2cadm_scan_t;
78
79 static void
i2cadm_scan_error(i2c_hdl_t * hdl,i2cadm_scan_t * scan)80 i2cadm_scan_error(i2c_hdl_t *hdl, i2cadm_scan_t *scan)
81 {
82 i2c_err_t err = i2c_err(hdl);
83 if (err != I2C_ERR_CONTROLLER) {
84 scan->scan_res = I2CADM_BUS_SCAN_ERROR;
85 scan->scan_error = strdup(i2c_errmsg(hdl));
86 if (scan->scan_error == NULL) {
87 scan->scan_error = "libi2c error; but failed to "
88 "duplicate libi2c error message";
89 }
90 return;
91 }
92
93 switch (i2c_ctrl_err(hdl)) {
94 case I2C_CTRL_E_ADDR_NACK:
95 case I2C_CTRL_E_DATA_NACK:
96 case I2C_CTRL_E_NACK:
97 scan->scan_res = I2CADM_BUS_SCAN_NO_DEV;
98 break;
99 case I2C_CTRL_E_REQ_TO:
100 scan->scan_res = I2CADM_BUS_SCAN_TIMEOUT;
101 break;
102 default:
103 scan->scan_res = I2CADM_BUS_SCAN_ERROR;
104 scan->scan_error = strdup(i2c_errmsg(hdl));
105 if (scan->scan_error == NULL) {
106 scan->scan_error = "i2c controller error; but "
107 "failed to duplicate libi2c error message";
108 }
109 break;
110 }
111 }
112
113 /*
114 * One does not simply scan an i2c device. In essence, we're trying to perform
115 * some I/O such that it will ack an address, but without causing the device to
116 * wreak havoc. Given the plethora of devices that are out there, this may not
117 * be possible. Safety cannot be guaranteed by construction.
118 *
119 * We basically do a one byte read from the device. Most devices will respond to
120 * this. The alternative that some other tools have done is to try to perform an
121 * SMBus Quick action.
122 */
123 static void
i2cadm_scan_one(i2c_hdl_t * hdl,i2c_port_t * port,i2cadm_scan_t * scan)124 i2cadm_scan_one(i2c_hdl_t *hdl, i2c_port_t *port, i2cadm_scan_t *scan)
125 {
126 i2c_io_req_t *req;
127 uint8_t data = 0x77;
128
129 if (!i2c_io_req_init(port, &req)) {
130 i2cadm_fatal("failed to initialize I/O request");
131 }
132
133 if (!i2c_io_req_set_addr(req, &scan->scan_addr)) {
134 i2cadm_fatal("failed to set scan address");
135 }
136
137 if (!i2c_io_req_set_receive_buf(req, &data, sizeof (data))) {
138 i2cadm_fatal("failed to set receive buffer");
139 }
140
141 if (i2c_io_req_exec(req)) {
142 scan->scan_res = I2CADM_BUS_SCAN_FOUND;
143 } else {
144 i2cadm_scan_error(hdl, scan);
145 }
146
147 i2c_io_req_fini(req);
148 }
149
150 typedef enum {
151 I2CADM_SCAN_ADDR,
152 I2CADM_SCAN_RESULT,
153 I2CADM_SCAN_ERROR
154 } i2cadm_scan_otype_t;
155
156 static boolean_t
i2cadm_scan_ofmt_cb(ofmt_arg_t * ofarg,char * buf,uint_t buflen)157 i2cadm_scan_ofmt_cb(ofmt_arg_t *ofarg, char *buf, uint_t buflen)
158 {
159 const i2cadm_scan_t *scan = ofarg->ofmt_cbarg;
160 const char *str;
161 size_t len;
162
163 switch (ofarg->ofmt_id) {
164 case I2CADM_SCAN_ADDR:
165 len = snprintf(buf, buflen, "0x%x", scan->scan_addr.ia_addr);
166 break;
167 case I2CADM_SCAN_RESULT:
168 switch (scan->scan_res) {
169 case I2CADM_BUS_SCAN_FOUND:
170 str = "found";
171 break;
172 case I2CADM_BUS_SCAN_NO_DEV:
173 str = "missing";
174 break;
175 case I2CADM_BUS_SCAN_RESERVED:
176 str = "reserved";
177 break;
178 case I2CADM_BUS_SCAN_TIMEOUT:
179 str = "timeout";
180 break;
181 case I2CADM_BUS_SCAN_ERROR:
182 str = "error";
183 break;
184 case I2CADM_BUS_SCAN_SKIPPED:
185 str = "skipped";
186 break;
187 default:
188 str = "unknown";
189 break;
190 }
191 len = strlcpy(buf, str, buflen);
192 break;
193 case I2CADM_SCAN_ERROR:
194 if (scan->scan_error != NULL) {
195 len = strlcpy(buf, scan->scan_error, buflen);
196 } else {
197 len = strlcpy(buf, "-", buflen);
198 }
199 break;
200 default:
201 return (B_FALSE);
202 }
203
204 return (len < buflen);
205 }
206
207 static const ofmt_field_t i2cadm_scan_ofmt[] = {
208 { "ADDR", 8, I2CADM_SCAN_ADDR, i2cadm_scan_ofmt_cb },
209 { "RESULT", 16, I2CADM_SCAN_RESULT, i2cadm_scan_ofmt_cb },
210 { "ERROR", 40, I2CADM_SCAN_ERROR, i2cadm_scan_ofmt_cb },
211 { NULL, 0, 0, NULL }
212 };
213
214 /*
215 * We can fit 16 devices on a line.
216 */
217 #define SCAN_DEVS_PER_LINE 0x10
218
219 static const char *key = ""
220 "\t- = No Device @ = Device Found\n"
221 "\tR = Reserved S = Skipped\n"
222 "\tX = Timed Out Err = Error\n";
223
224 static bool
i2cadm_scan_table_cb(void * arg,uint16_t i)225 i2cadm_scan_table_cb(void *arg, uint16_t i)
226 {
227 const i2cadm_scan_t *results = arg;
228 const char *msgs[] = { "???", "@", "-", "R", "X", "Err", "S" };
229
230 (void) printf("%3s", msgs[results[i].scan_res]);
231
232 return (results[i].scan_res == I2CADM_BUS_SCAN_ERROR);
233 }
234
235 static void
i2cadm_scan_table_post(void * arg,uint16_t max_addr)236 i2cadm_scan_table_post(void *arg, uint16_t max_addr)
237 {
238 const i2cadm_scan_t *results = arg;
239 (void) printf("\nErrors\n");
240 for (uint16_t i = 0; i < max_addr; i++) {
241 if (results[i].scan_res != I2CADM_BUS_SCAN_ERROR)
242 continue;
243
244 if (max_addr > UINT8_MAX) {
245 (void) printf("0x%03x: %s\n", i, results[i].scan_error);
246 } else {
247 (void) printf("0x%02x: %s\n", i, results[i].scan_error);
248 }
249 }
250 }
251
252 int
i2cadm_scan(int argc,char * argv[])253 i2cadm_scan(int argc, char *argv[])
254 {
255 int c;
256 i2c_port_t *port;
257 bool ten_bit = false;
258 uint16_t max_addr = 1 << 7;
259 i2cadm_scan_t *results;
260 uint16_t *dev_addrs = NULL;
261 size_t naddrs = 0, nalloc = 0;
262 boolean_t parse = B_FALSE;
263 uint_t flags = 0;
264 const char *fields = NULL;
265 ofmt_status_t oferr;
266 ofmt_handle_t ofmt;
267
268 /*
269 * In the future we should consider -T for 10-bit addressing.
270 */
271 while ((c = getopt(argc, argv, ":d:Ho:p")) != -1) {
272 switch (c) {
273 case 'd': {
274 if (naddrs == nalloc) {
275 nalloc += 8;
276 dev_addrs = recallocarray(dev_addrs, naddrs,
277 nalloc, sizeof (uint16_t));
278 if (dev_addrs == NULL) {
279 err(EXIT_FAILURE, "failed to allocate "
280 "memory for %zu I2C addresses",
281 nalloc);
282 }
283 }
284
285 const char *err;
286 long long l = strtonumx(optarg, 0, max_addr - 1, &err,
287 0);
288 if (err != NULL) {
289 errx(EXIT_FAILURE, "invalid device address %s: "
290 "address is %s", optarg, err);
291 }
292 dev_addrs[naddrs] = (uint16_t)l;
293 naddrs++;
294 break;
295 }
296 case 'H':
297 flags |= OFMT_NOHEADER;
298 break;
299 case 'o':
300 fields = optarg;
301 break;
302 case 'p':
303 parse = B_TRUE;
304 flags |= OFMT_PARSABLE;
305 break;
306 case ':':
307 i2cadm_scan_help("option -%c requires an argument",
308 optopt);
309 exit(EXIT_USAGE);
310 case '?':
311 i2cadm_scan_help("unknown option: -%c", optopt);
312 exit(EXIT_USAGE);
313 }
314 }
315
316 argv += optind;
317 argc -= optind;
318 if (argc == 0) {
319 errx(EXIT_USAGE, "missing required port to scan");
320 } else if (argc > 1) {
321 errx(EXIT_USAGE, "encountered extraneous arguments starting "
322 "with %s", argv[1]);
323 }
324
325 if (parse && fields == NULL) {
326 errx(EXIT_USAGE, "-p requires fields specified with -o");
327 }
328
329 if (flags != 0 && fields == NULL) {
330 errx(EXIT_USAGE, "-H can only be used with -o");
331 }
332
333 if (fields != NULL) {
334 if (!parse) {
335 flags |= OFMT_WRAP;
336 }
337
338 oferr = ofmt_open(fields, i2cadm_scan_ofmt, flags, 0,
339 &ofmt);
340 ofmt_check(oferr, parse, ofmt, i2cadm_ofmt_errx, warnx);
341 }
342
343 if (!i2c_port_init_by_path(i2cadm.i2c_hdl, argv[0], &port)) {
344 i2cadm_fatal("failed to parse port path %s", argv[0]);
345 }
346
347 results = calloc(max_addr, sizeof (i2cadm_scan_t));
348 if (results == NULL) {
349 err(EXIT_FAILURE, "failed to allocate scan results tracking "
350 "structure");
351 }
352
353 /*
354 * If we have a specific device list, then mark everything skipped and
355 * come back and mark the specific instances we care about as things to
356 * check.
357 */
358 if (dev_addrs != NULL) {
359 for (uint16_t i = 0; i < max_addr; i++) {
360 results[i].scan_res = I2CADM_BUS_SCAN_SKIPPED;
361 }
362
363 for (uint16_t i = 0; i < naddrs; i++) {
364 results[dev_addrs[i]].scan_res =
365 I2CADM_BUS_SCAN_UNKNOWN;
366 }
367 }
368
369 for (uint16_t i = 0; i < max_addr; i++) {
370 i2cadm_scan_t *scan = &results[i];
371
372 scan->scan_addr.ia_type = ten_bit ? I2C_ADDR_10BIT :
373 I2C_ADDR_7BIT;
374 scan->scan_addr.ia_addr = i;
375
376 if (scan->scan_res == I2CADM_BUS_SCAN_SKIPPED)
377 continue;
378
379 /*
380 * Determine if this is a reserved address or not.
381 */
382 if (i2c_addr_reserved(&scan->scan_addr)) {
383 scan->scan_res = I2CADM_BUS_SCAN_RESERVED;
384 continue;
385 }
386
387 i2cadm_scan_one(i2cadm.i2c_hdl, port, scan);
388 }
389
390 if (fields == NULL) {
391 i2cadm_table_t table = {
392 .table_port = argv[0],
393 .table_key = key,
394 .table_msg = "Device scan on",
395 .table_max = max_addr,
396 .table_cb = i2cadm_scan_table_cb,
397 .table_post = i2cadm_scan_table_post
398 };
399 i2cadm_print_table(&table, results);
400 } else {
401 for (uint16_t i = 0; i < max_addr; i++) {
402 ofmt_print(ofmt, &results[i]);
403 }
404 ofmt_close(ofmt);
405 }
406
407 for (uint16_t i = 0; i < max_addr; i++) {
408 free(results[i].scan_error);
409 }
410 free(results);
411 i2c_port_fini(port);
412 return (EXIT_SUCCESS);
413 }
414