mcp2221-hal 0.1.0

Driver for the MCP2221 USB to UART, I2C and GPIO converter
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
use std::cell::Cell;
use std::time::Duration;

use hidapi::{HidApi, HidDevice};

use crate::Error;
use crate::analog::{AdcReading, VoltageReference};
use crate::commands::{FlashDataSubCode, McpCommand, UsbReport};
use crate::constants::{COMMAND_SUCCESS, MCP2221_PID, MICROCHIP_VID};
use crate::gpio::{GpioChanges, GpioValues, Pins};
use crate::i2c::{I2cCancelTransferResponse, I2cSpeed, ReadType, WriteType};
use crate::settings::{
    ChipSettings, DeviceString, GpSettings, InterruptSettingsChanges, SramSettingsChanges,
};
use crate::status::Status;

mod i2c_eh;

/// Driver for the MCP2221.
///
/// # Overview
///
/// All of the functionality of this crate is exposed by methods on this struct. You
/// will likely prefer to use the [`embedded_hal`] trait implementations, however,
/// and certainly if you are using the MCP2221 for driver development.
///
/// There are _a lot_ of methods. All of the MCP2221's functions are exposed through
/// USB HID commands, so it made sense to have this be the single object that performs
/// those USB command transfers.
///
/// To help group methods, they are prefixed with their general function, at the
/// expense of making the names slightly awkward:
///
/// - I2C-related methods start with `i2c_`.
/// - GPIO-related methods start with `gpio_`.
/// - Analog IO methods start with `analog_`.
/// - Settings-related methods are named for their location.
///     - Flash memory settings, which generally affect the initial device behaviour,
///       can be read and altered with the `flash_` methods.
///     - SRAM settings, which affect the behaviour of the running chip but are not
///       persisted across reset, can be read and altered with the `sram_` methods.
/// - Methods to check the MCP2221 interrupt flag start with `interrupt_`.
/// - Strings that affect the USB device enumeration can be read and changed with
///   the `usb_` methods.
///
/// # Construction
///
/// Construct an instance an instance of this driver with [`MCP2221::connect`], which
/// will attempt to open a USB device with the default vendor ID (VID) and product ID
/// (PID). If you have changed either the VID or PID, use
/// [`MCP2221::connect_with_vid_and_pid`].
///
/// It is _not yet_ supported to connect to the USB device by serial number, so that you
/// may have multiple MCP2221 devices with the same VID and PID but connect based on
/// differing serials.
///
/// # `embedded-hal`
///
/// ## I2C
///
/// This struct implements the [blocking I2c trait](embedded_hal::i2c::I2c) as well as
/// the [async I2C trait](embedded_hal_async::i2c::I2c). It has no mutable state, so you
/// can pass a `&MCP2221` to anything expecting `impl I2c`. The async trait
/// implementation blocks, but it allows you to use or write async drivers that work as
/// expected.
///
/// Please note the limitation of the [`I2c::transaction`] implementation mentioned in
/// the [crate docs](crate), in that reads cannot precede writes due to the limitations
/// of the MCP2221's available commands. In practice, we've found `transaction` is used
/// to perform a write-read or to write from multiple buffers, both of which work as you
/// would expect.
///
/// [`I2c::transaction`]: embedded_hal::i2c::I2c::transaction
///
/// ## GPIO (digital IO)
///
/// The [`Input`] struct implements [`embedded_hal::digital::InputPin`], and [`Output`]
/// implements both [`OutputPin`] and [`StatefulOutputPin`].
///
/// [`embedded_hal_async::digital::Wait`] is _not_ supported. It would be possible to
/// implement at the expense of busy-waiting. If you need this, please [open an issue].
///
/// You can get instances of each by taking the pins from the driver with
/// [`MCP2221::gpio_take_pins`], which can be called only once (so you don't end up with
/// two structs trying to control the same pin). The returned structure contains a
/// [`GpPin`] for each of the four GP pins on the MCP2221; this is a pin with an
/// unspecified mode. Call `try_into()` or either of the two conversion methods, which
/// will put the pin into GPIO mode and return an [`Input`] or [`Output`] as desired.
///
/// Be warned that you can still use the GPIO methods of this driver struct to change
/// pin modes or GPIO direction while holding onto the pin structs, which will lead
/// to errors. See the [`gpio`] module for more information and usage examples.
///
/// # Settings
///
/// Settings are divided between [`GpSettings`], which control the behaviour of the four
/// GP (general-purpose) pins, and [`ChipSettings`], which control everything else.
/// Settings stored in flash take effect only after reset, and only at power-up, while
/// changes to the SRAM settings affect the behaviour of the running device. See the
/// [`settings`](crate::settings) module documentation for more details.
///
/// The following settings methods are available:
///
/// - [`MCP2221::flash_read_chip_settings`]
/// - [`MCP2221::flash_write_chip_settings`]
/// - [`MCP2221::flash_read_gp_settings`]
/// - [`MCP2221::flash_write_gp_settings`]
/// - [`MCP2221::sram_read_settings`]
/// - [`MCP2221::sram_write_settings`]
/// - [`MCP2221::sram_write_gp_settings`]
///
/// Reflecting the underlying interface, chip and GP settings are separately read from
/// (and written to) flash, while they are read together from SRAM. There is a method
/// to separately write the GP settings to flash; this is to work around a bug that
/// otherwise resets the analog voltage references.
///
/// As well, there are three strings that are presented on USB enumeration. Because
/// these are changed individually they have their own methods:
///
/// - [`MCP2221::usb_manufacturer`]
/// - [`MCP2221::usb_change_manufacturer`]
/// - [`MCP2221::usb_product`]
/// - [`MCP2221::usb_change_product`]
/// - [`MCP2221::usb_serial_number`]
/// - [`MCP2221::usb_change_serial_number`]
///
/// If you enable [CDC serial number enumeration], the serial port presented by the
/// MCP2221 USB CDC device will have a stable name (which, at least on POSIX systems,
/// you can customise by changing the serial number).
///
/// Note that these USB-related strings all use [`DeviceString`], which is just a
/// Unicode string that must be 60 bytes or fewer when encoded as UTF-16.
///
/// # GPIO
///
/// Outside of the [`embedded_hal`] traits, access to the pins in GPIO mode goes
/// through [`MCP2221::gpio_read`] and [`MCP2221::gpio_write`], which respectively
/// read and write potentially all the pins at once.
///
/// # Analog IO
///
/// The MCP2221 has a three-channel 10-bit analog-to-digital converter (ADC) for reading
/// voltages on pins GP1, GP2, and GP3. It has a single-output 5-bit digital-to-analog
/// converter that can set a voltage level on GP2 or GP3. See the [`analog`] module
/// for details.
///
/// Analog voltages can be read with [`MCP2221::analog_read`], and the output of the
/// DAC can be set with [`MCP2221::analog_write`]. You can configure the range of
/// each in SRAM (ie not persisted) with [`MCP2221::analog_set_input_reference`]
/// and [`MCP2221::analog_set_output_reference`]. Changing the references in flash is
/// done through the usual writing of [`ChipSettings`] to flash.
///
/// Please note the firmware bugs mentioned in the [`analog`] module documentation,
/// as well as the notes there on the DAC's output range.
///
/// # I2C
///
/// This driver exposes all of the different read and write options offered by the
/// MCP2221:
///
/// - [`MCP2221::i2c_read`] performs a normal read with a Start and Stop condition.
/// - [`MCP2221::i2c_read_repeated_start`] is intended to follow an I2C operation with
///   no final Stop condition, though it's actual behaviour is not entirely clear.
/// - [`MCP2221::i2c_write`] performs a normal write with a Start and Stop condition.
/// - [`MCP2221::i2c_write_repeated_start`] is intended to follow an I2C operation with
///   no final Stop condition, but again its actual behaviour is unclear.
/// - [`MCP2221::i2c_write_no_stop`] performs a write with no final stop condition.
///
/// Please see the documentation for each method. In particular, the semantics of the
/// repeated-Start methods as understood by Microchip are not clear from the datasheet.
/// Note that there is no method to read without a final Stop condition, which is why
/// this driver cannot fulfil the contract of [`embedded_hal::i2c::I2c::transaction`]
/// in its most general case.
///
/// The I2C bus speed can be set with [`MCP2221::i2c_set_bus_speed`]. The standard
/// speeds of 400 kbit/s and 100 kbit/s work as expected. However, the slowest possible
/// speed is just under 47 kbit/s, and not every speed between that and 400 kbit/s can
/// be achieved exactly due to the way the speed is set internally.
///
/// Should the I2C bus gets into a bad state, [`MCP2221::i2c_cancel_transfer`] may help.
/// If you wish to check that a device responds to its address, use
/// [`MCP2221::i2c_check_address`].
///
/// # Interrupt detection
///
/// Please note that interrupts as you normally understand them cannot be achieved with
/// this driver. Rather, the MCP2221 can detect positive and negative edges when GP1 is
/// set to the appropriate mode, and then set a flag which can be checked.
///
/// Use [`MCP2221::interrupt_detected`] to check the interrupt flag, and then
/// [`MCP2221::interrupt_clear`] to clear the flag. Interrupt settings (GP1 mode, which
/// edges to trigger on) are configured through the usual settings methods.
///
/// # Miscellaneous
///
/// The MCP2221 can be reset with [`MCP2221::reset`], after which you will need to
/// attempt to reconnect to the device.
///
/// The current status of the device can be read with [`MCP2221::status`]. The only
/// information in the returned [`Status`] struct that isn't exposed through more
/// convenient methods is I2C engine state of questionable usefulness, and the device
/// hardware and firmware revision codes.
///
/// The device's USB information _as seen by the host_ can be accessed through the
/// [`MCP2221::usb_device_info`] method.
///
/// The device's factory serial number (not the USB serial number) can be read through
/// the [`MCP2221::factory_serial_number`] method. It seems this is always the ASCII
/// text "01234567".
///
/// [open an issue]: https://github.com/robjwells/mcp2221-hal/issues
/// [`gpio`]: crate::gpio
/// [`Input`]: crate::gpio::Input
/// [`Output`]: crate::gpio::Input
/// [`OutputPin`]: embedded_hal::digital::OutputPin
/// [`StatefulOutputPin`]: embedded_hal::digital::StatefulOutputPin
/// [`GpPin`]: crate::gpio::GpPin
/// [`analog`]: crate::analog
/// [CDC serial number enumeration]: ChipSettings::cdc_serial_number_enumeration_enabled
#[derive(Debug)]
pub struct MCP2221 {
    /// Underlying [`hidapi`] device.
    ///
    /// The C hidapi library is not thread safe (`cargo test` will trigger a crash)
    /// and the `hidapi` types are appropriately `!Sync`.
    inner: HidDevice,
    /// Marker for whether the pin structs have been taken from the driver.
    ///
    /// This is used to fake "moving" the pins out of the driver, but really everything
    /// has a shared reference to the driver under the covers. The `Cell` is used to
    /// maintain requirement of only a shared reference. It is safe since the driver is
    /// `!Sync` anyway. See [`Self::gpio_take_pins`] for the only place it is used.
    pins_taken: Cell<bool>,
}

impl MCP2221 {
    ////////////////////////////////////////////////////////////////////////////////
    // Constructors - USB methods
    ////////////////////////////////////////////////////////////////////////////////

    /// Connect to the first USB device found with the default vendor and product ID.
    ///
    /// The default VID is 1240 (0x4D8) and PID 221 (0xDD) for both the original
    /// MCP2221 and the (more common) MCP2221A.
    ///
    /// # Errors
    ///
    /// An error will be returned if the USB device cannot be opened.
    pub fn connect() -> Result<Self, Error> {
        MCP2221::connect_with_vid_and_pid(MICROCHIP_VID, MCP2221_PID)
    }

    // TODO: connect to a device via serial number.

    /// Connect to the first USB device found with the given vendor and product ID.
    ///
    /// Use this constructor if you have changed the USB VID or PID of your MCP2221.
    ///
    /// # Errors
    ///
    /// An error will be returned if the USB device cannot be opened.
    pub fn connect_with_vid_and_pid(vendor_id: u16, product_id: u16) -> Result<Self, Error> {
        let hidapi = HidApi::new()?;
        let device = hidapi.open(vendor_id, product_id)?;
        Ok(Self {
            inner: device,
            pins_taken: Cell::new(false),
        })
    }

    ////////////////////////////////////////////////////////////////////////////////
    // USB report exchange with the MCP2221
    ////////////////////////////////////////////////////////////////////////////////

    /// Write the given command to the MCP and read the 64-byte response.
    ///
    /// Returning an optional buffer is not great for the callers' ergonomics
    /// but it's the most straightforward way of representing the non-response
    /// from Reset Chip that doesn't return an empty array.
    fn transfer(&self, command: &UsbReport) -> Result<Option<[u8; 64]>, Error> {
        let out_command_byte = command.write_buffer[0];
        let written = self.inner.write(&command.report_bytes())?;
        if command.has_no_response() {
            return Ok(None);
        }

        let mut read_buffer = [0u8; 64];
        let read = self.inner.read(&mut read_buffer)?;
        let read_command_byte = read_buffer[0];

        // Check length written and read.
        assert_eq!(written, 65, "Didn't write full report.");
        assert_eq!(read, 64, "Didn't read full report.");

        // Check command-code echo.
        if read_command_byte != out_command_byte {
            return Err(Error::MismatchedCommandCodeEcho {
                sent: out_command_byte,
                received: read_command_byte,
            });
        }

        let status_code = read_buffer[1];
        if status_code == COMMAND_SUCCESS {
            Ok(Some(read_buffer))
        } else {
            // Command has failed, so we check the command to see if there is a more
            // specific Error case, otherwise we return the most general one, and
            // enclose the failure code.
            command
                .check_error_code(status_code)
                .and(Err(Error::CommandFailed(status_code)))
        }
    }

    ////////////////////////////////////////////////////////////////////////////////
    // MCP2221 general commands
    ////////////////////////////////////////////////////////////////////////////////

    /// Reset the MCP2221.
    ///
    /// This can be useful after changing settings in the device's flash memory,
    /// which only take effect on power-up.
    ///
    /// Resetting the chip causes the device to re-enumerate with the USB host,
    /// so you will need to create a new driver struct afterwards.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.15 for the underlying Reset Chip HID command, and section
    /// 4.2.3 for reset timings.
    pub fn reset(self) -> Result<(), Error> {
        let mut command = UsbReport::new(McpCommand::ResetChip);
        command.set_data_byte(2, 0xCD);
        command.set_data_byte(3, 0xEF);

        self.transfer(&command)?;
        Ok(())
    }

    /// Read the status of the MCP2221.
    ///
    /// The returned structure includes the current status of the I2C engine, and the
    /// hardware and firmware revision numbers.
    ///
    /// It includes the raw ADC channel readings, but you should prefer to use
    /// [`MCP2221::analog_read`].
    ///
    /// It also contains the edge-triggered interrupt flag, but you should prefer to
    /// use [`MCP2221::interrupt_detected`].
    ///
    /// # Datasheet
    ///
    /// See section 3.11 of the datasheet for the underlying Status/Set Parameters
    /// HID command.
    pub fn status(&self) -> Result<Status, Error> {
        let buf = self
            .transfer(&UsbReport::new(McpCommand::StatusSetParameters))?
            .expect("Always has response buffer.");
        Ok(Status::from_buffer(&buf))
    }

    ////////////////////////////////////////////////////////////////////////////////
    // I2C
    ////////////////////////////////////////////////////////////////////////////////

    /// Set the speed of the I2C bus.
    ///
    /// The MCP2221 can communicate at speeds from just below 47 kbit/s to 400 kbit/s,
    /// though not every rate can be achieved exactly due to the way the speed is
    /// set in the device.
    ///
    /// # Errors
    ///
    /// An [`Error::I2cCouldNotChangeSpeed`] may be returned if an ongoing I2C transfer
    /// prevented the device from setting the bus speed.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.1 of the datasheet for the underlying Status/Set Parameters
    /// HID command.
    pub fn i2c_set_bus_speed(&self, speed: I2cSpeed) -> Result<(), Error> {
        let mut uc = UsbReport::new(McpCommand::StatusSetParameters);
        // When this value is put in this field, the device will take the next command
        // field and interpret it as the system clock divider that will give the
        // I2C/SMBus communication clock.
        uc.set_data_byte(3, 0x20);
        uc.set_data_byte(4, speed.to_clock_divider());
        let read_buffer = self.transfer(&uc)?.expect("Always has response buffer.");
        match read_buffer[3] {
            0x20 => Ok(()),
            0x21 => Err(Error::I2cCouldNotChangeSpeed),
            _ => unreachable!("Invalid response from MCP2221 for I2C speed set command."),
        }
    }

    /// Cancel current I2C transfer.
    ///
    /// If the I2C engine is busy, the driver will attempt to cancel the current
    /// transfer.
    ///
    /// Microchip's Android Java driver for the MCP2221 describes this command
    /// as "forc\[ing\] a STOP condition into the SCL/SDA lines".
    ///
    /// <div class="warning">
    ///
    /// The driver will not instruct the MCP2221 to cancel a transfer if the I2C engine
    /// appears idle, as doing so appears to put the I2C engine into a busy state.
    ///
    /// </div>
    ///
    /// # Datasheet
    ///
    /// See section 3.11 of the datasheet for the underlying Status/Set Parameters
    /// HID command.
    pub fn i2c_cancel_transfer(&self) -> Result<I2cCancelTransferResponse, Error> {
        // Only issue the cancellation command if the I2C engine is busy to avoid it
        // _becoming_ busy by issuing the cancellation.
        if self.status()?.i2c.communication_state.is_idle() {
            return Ok(I2cCancelTransferResponse::NoTransfer);
        }

        let mut uc = UsbReport::new(McpCommand::StatusSetParameters);
        uc.set_data_byte(2, 0x10);
        let read_buffer = self.transfer(&uc)?.expect("Always has response buffer.");

        match read_buffer[2] {
            0x10 => Ok(I2cCancelTransferResponse::MarkedForCancellation),
            0x11 => Ok(I2cCancelTransferResponse::NoTransfer),
            0x00 => Ok(I2cCancelTransferResponse::Done),
            code => unreachable!("Unknown code received from I2C cancel attempt {code}"),
        }
    }

    /// Read data from an I2C target.
    ///
    /// The address must be the 7-bit address, not an 8-bit read or write address.
    /// 10-bit addresses are not currently supported.
    ///
    /// Zero-length transfers are not accepted, as they can cause the target to lock up
    /// the I2C bus if it holds SDA low for the first bit.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.8 for the underlying I2C Read Data HID command.
    pub fn i2c_read(&self, seven_bit_address: u8, read_buffer: &mut [u8]) -> Result<(), Error> {
        self._i2c_read(seven_bit_address, read_buffer, ReadType::Normal)
    }

    /// Read data from an I2C target with a repeated START condition.
    ///
    /// It is unclear from the datasheet how this differs from the standard I2C read HID
    /// command or how it should be used. Formally, a repeated-START in I2C is just a
    /// START condition when the previous transfer has not been terminated by a STOP
    /// condition, so this _should_ be the same as issuing a normal read.
    ///
    /// In this library, this method is called after writing with no stop, in order to
    /// perform a write-read (ST, address-w, data-out, SR, address-r, data-in, SP). It
    /// is exposed to users for completeness with no guarantees or suggestions about its
    /// usage.
    ///
    /// In general, it appears that this exposes some of the internal details of the
    /// MCP2221 I2C engine, but without the explanation needed to make sense of it.
    ///
    /// The restrictions from [`MCP2221::i2c_read`] also apply: the address provided
    /// must be the 7-bit address, and zero-length transfers are not supported.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.9 for the underlying I2C Read Data Repeated-START HID command.
    pub fn i2c_read_repeated_start(
        &self,
        seven_bit_address: u8,
        read_buffer: &mut [u8],
    ) -> Result<(), Error> {
        self._i2c_read(seven_bit_address, read_buffer, ReadType::RepeatedStart)
    }

    /// Cancel the I2C transfer if the target device did not acknowledge its address.
    ///
    /// This is an internal helper method.
    fn i2c_bail_for_nack(&self) -> Result<(), Error> {
        match self.status()?.i2c.target_acknowledged_address {
            true => Ok(()),
            false => {
                self.i2c_cancel_transfer()?;
                Err(Error::I2cAddressNack)
            }
        }
    }

    /// Perform an I2C read of the type specified.
    ///
    /// The I2C HID commands only differ in their command bytes (and their semantics),
    /// so this is the underlying implementation for the two i2c_read_ functions.
    ///
    /// Starts with an underscore purely so users can have the obvious `i2c_read()`
    /// for doing normal reads.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.8 (normal read) and 3.1.9 (repeated start).
    fn _i2c_read(
        &self,
        seven_bit_address: u8,
        read_buffer: &mut [u8],
        read_type: ReadType,
    ) -> Result<(), Error> {
        // Don't attempt to read if the transfer length is 0, as attempting a zero-length
        // read will lock up the bus if the peripheral pulls SDA low trying to transmit.
        // Note the MCP2221 will happily let you do that!
        if read_buffer.is_empty() {
            return Err(Error::I2cTransferEmpty);
        }
        let Ok(tx_len): Result<u16, _> = read_buffer.len().try_into() else {
            return Err(Error::I2cTransferTooLong);
        };

        use crate::i2c::I2cAddressing;
        let mut read_command = UsbReport::new(read_type.into());
        let [tx_len_low, tx_len_high] = tx_len.to_le_bytes();
        read_command.set_data_byte(1, tx_len_low);
        read_command.set_data_byte(2, tx_len_high);
        read_command.set_data_byte(3, seven_bit_address.into_read_address());
        self.transfer(&read_command)?;
        // Clean up if the target did not acknowledge.
        self.i2c_bail_for_nack()?;
        self.i2c_read_get_data(read_buffer)
    }

    /// Read I2C target data back from the MCP2221.
    ///
    /// This is called after requesting a read to get the actual data.
    ///
    /// It appears in the datasheet as a separate HID command but it is really just
    /// an implementation detail due to the way the MCP2221 does reads. Writes don't
    /// have the same issue-the-command/perform-the-command split.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.10 for the underlying I2c Read Data - Get I2C data command.
    fn i2c_read_get_data(&self, read_buffer: &mut [u8]) -> Result<(), Error> {
        // Retries are necessary because it is likely the host will request data from
        // the MCP2221 faster than it can process it, in which case it returns the
        // "error reading from the engine" 0x41 code.
        //
        // It may be necessary to turn this into a configuration option for the driver
        // if users encounter failed reads.
        const MAX_RETRIES: u8 = 20;
        // With my Pico test setup, for a read of 65,535 bytes, a 2ms delay upon
        // failing to read from the I2C engine appears to yield the shortest overall
        // time (compared to 1ms and 3ms). Adding a 1ms delay after each successful
        // read (hoping to avoid the 2ms failure delay) just seems to increase the
        // overall time taken.
        const RETRY_DELAY: Duration = Duration::from_millis(2);
        // Sanity check that the driver never tries to read zero bytes.
        if read_buffer.is_empty() {
            return Err(Error::I2cTransferEmpty);
        }

        let transfer_length = read_buffer.len();
        let mut read_so_far: usize = 0;

        let get_command = UsbReport::new(McpCommand::I2cGetData);
        let mut retries = MAX_RETRIES;

        while read_so_far < transfer_length {
            match self.transfer(&get_command) {
                Ok(Some(buffer)) => {
                    // Reset the number of retries.
                    retries = MAX_RETRIES;
                    if buffer[3] == 127 {
                        // Error occurred when reading the data, try again.
                        // This shouldn't occur when the status at byte 1 is OK.
                        continue;
                    }
                    let data_length = buffer[3] as usize;
                    read_buffer[read_so_far..read_so_far + data_length]
                        .copy_from_slice(&buffer[4..4 + data_length]);
                    read_so_far += data_length;
                }
                Ok(None) => unreachable!("Get Data always returns a buffer."),
                Err(Error::I2cEngineReadError) if retries > 0 => {
                    // Error reading target data from the I2C engine, just try again
                    // after a short delay.
                    retries -= 1;
                    std::thread::sleep(RETRY_DELAY);
                    continue;
                }
                e @ Err(_) => {
                    // Out of retries, just return the error.
                    e?;
                }
            }
        }

        Ok(())
    }

    /// Write data to an I2C target.
    ///
    /// The address must be the 7-bit address, not an 8-bit read or write address.
    /// 10-bit addresses are not currently supported.
    ///
    /// The given `data` buffer cannot be more than 65,535 bytes long, as this is the
    /// maximum transfer size supported by the MCP2221.
    ///
    /// Zero-length writes are not accepted, use [`MCP2221::i2c_check_address`] instead
    /// if you are trying to scan the bus.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.5 for the underlying I2C Write Data HID command.
    pub fn i2c_write(&self, seven_bit_address: u8, write_buffer: &[u8]) -> Result<(), Error> {
        self._i2c_write(seven_bit_address, write_buffer, WriteType::Normal)
    }

    /// Write data to an I2C target with a repeated START condition.
    ///
    /// It is unclear from the datasheet how this differs from the standard I2C write
    /// HID command or how it should be used. Formally, a repeated-START in I2C is just a
    /// START condition when the previous transfer has not been terminated by a STOP
    /// condition, so this _should_ be the same as issuing a normal write.
    ///
    /// This method is not actually used in the implementation of this library, and is
    /// only exposed because the MCP2221 exposes it as a separate USB HID command. No
    /// guarantees or suggestions are made about its usage. (But if you discover
    /// something that might help others, please [file an issue].)
    ///
    /// [file an issue]: https://github.com/robjwells/mcp2221-hal/issues
    ///
    /// The restrictions from [`MCP2221::i2c_write`] also apply: the address provided
    /// must be the 7-bit address, and zero-length transfers are not supported.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.6 for the underlying I2C Write Data Repeated-START HID command.
    pub fn i2c_write_repeated_start(
        &self,
        seven_bit_address: u8,
        write_buffer: &[u8],
    ) -> Result<(), Error> {
        self._i2c_write(seven_bit_address, write_buffer, WriteType::RepeatedStart)
    }

    /// Write data to an I2C target without a final STOP condition.
    ///
    /// In this library, this is used to implement I2C write-read (ST, address-w,
    /// data-out, SR, address-r, data-in, SP) before a read with repeated-START.
    /// It is exposed to the user for completeness with no guarantees or suggestions
    /// about its usage outside of this scenario.
    ///
    /// The restrictions from [`MCP2221::i2c_write`] still apply: the address provided
    /// must be the 7-bit address, and zero-length transfers are not supported.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.7 for the underlying I2C Write Data NO STOP HID command.
    pub fn i2c_write_no_stop(
        &self,
        seven_bit_address: u8,
        write_buffer: &[u8],
    ) -> Result<(), Error> {
        self._i2c_write(seven_bit_address, write_buffer, WriteType::NoStop)
    }

    /// Perform an I2C write of the type specified.
    ///
    /// The I2C HID commands only differ in their command bytes (and their semantics),
    /// so this is the underlying implementation for the three i2c_write_ functions.
    ///
    /// Starts with an underscore purely so users can have the obvious `i2c_write()`
    /// for doing normal write.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.5 (normal write), 3.1.6 (repeated-START), and 3.1.7 (no STOP).
    fn _i2c_write(
        &self,
        seven_bit_address: u8,
        write_buffer: &[u8],
        write_type: WriteType,
    ) -> Result<(), Error> {
        let Ok([tx_len_low, tx_len_high]) = u16::try_from(write_buffer.len()).map(u16::to_le_bytes)
        else {
            return Err(Error::I2cTransferTooLong);
        };
        if write_buffer.is_empty() {
            return Err(Error::I2cTransferEmpty);
        }

        use crate::i2c::I2cAddressing;
        let mut command = UsbReport::new(write_type.into());
        command.set_data_byte(1, tx_len_low);
        command.set_data_byte(2, tx_len_high);
        command.set_data_byte(3, seven_bit_address.into_write_address());

        // Retries appear less necessary than when reading, but the host can still
        // attempt to write faster than the MCP2221 can accept them, so we set a retry
        // limit to avoid a potentially infinite loop.
        const MAX_RETRIES: u8 = 20;
        const RETRY_DELAY: Duration = Duration::from_millis(2);

        for (idx, chunk) in write_buffer.chunks(60).enumerate() {
            let mut retries = MAX_RETRIES;
            loop {
                command.write_buffer[4..4 + chunk.len()].copy_from_slice(chunk);
                match self.transfer(&command) {
                    Ok(_) => {
                        if idx == 0 {
                            // Check for address acknowledgement, otherwise clean up,
                            // but only for the first chunk. MCP2221 will happily
                            // take writes for a missing target.
                            self.i2c_bail_for_nack()?;
                        }
                        break;
                    }
                    Err(Error::I2cEngineBusy) if retries > 0 => {
                        retries -= 1;
                        std::thread::sleep(RETRY_DELAY);
                        continue;
                    }
                    e @ Err(_) => e?,
                };
            }
        }

        Ok(())
    }

    /// Perform an I2C write-read to the given target address.
    ///
    /// First the contents of `write_buffer` are written to the target, without a final
    /// STOP condition. Then a repeated-START is issued and enough bytes are read from
    /// the target to fill `read_buffer`.
    ///
    /// # Datasheet
    ///
    /// See sections 3.1.7 (write, no STOP) and 3.1.9 (read, repeated START) for the
    /// underlying HID commands.
    pub fn i2c_write_read(
        &self,
        seven_bit_address: u8,
        write_buffer: &[u8],
        read_buffer: &mut [u8],
    ) -> Result<(), Error> {
        self.i2c_write_no_stop(seven_bit_address, write_buffer)?;
        self.i2c_read_repeated_start(seven_bit_address, read_buffer)
    }

    /// Check if an I2C target acknowledges the given address.
    ///
    /// This is a special-case of an I2C write, where no bytes are actually written
    /// to the target. It is a separate function because of the need to potentially
    /// also cancel the I2C transfer after the write if the device does not respond.
    pub fn i2c_check_address(&self, seven_bit_address: u8) -> Result<bool, Error> {
        use crate::i2c::I2cAddressing;
        let mut command = UsbReport::new(McpCommand::I2cWriteData);
        command.set_data_byte(3, seven_bit_address.into_write_address());

        const MAX_RETRIES: u8 = 20;
        const RETRY_DELAY: Duration = Duration::from_millis(2);
        for _ in 0..MAX_RETRIES {
            match self.transfer(&command) {
                Ok(_) => {
                    // The write was submitted, doesn't mean the target is there.
                    let address_ack = self.status()?.i2c.target_acknowledged_address;
                    // Clean up any incomplete transfer.
                    self.i2c_cancel_transfer()?;
                    return Ok(address_ack);
                }
                Err(Error::I2cEngineBusy) => {
                    // Just try again after a delay.
                    std::thread::sleep(RETRY_DELAY);
                    continue;
                }
                e @ Err(_) => {
                    // Some other error that we weren't expecting.
                    e?;
                }
            };
        }
        // If we get here we ran out of retries without checking the address.
        Err(Error::I2cOperationFailed)
    }

    ////////////////////////////////////////////////////////////////////////////////
    // GPIO
    ////////////////////////////////////////////////////////////////////////////////

    /// Take the four GP pin structs for individual GPIO operation.
    ///
    /// This can only be done once, and will return `None` afterwards.
    pub fn gpio_take_pins(&self) -> Option<Pins> {
        if self.pins_taken.get() {
            None
        } else {
            self.pins_taken.set(true);
            Some(Pins::new(self))
        }
    }

    /// Get GPIO pin direction and current logic levels.
    ///
    /// The logic level listed for input pins is the value read at that pin, and for
    /// output pins it is the currently set output. Only pins that are configured for
    /// GPIO operation are present in the returned struct.
    ///
    /// <div class="warning">
    ///
    /// You should prefer this method over [`MCP2221::sram_read_settings`] to read the
    /// state of the GPIO pins as that does not provide input pin readings (the level
    /// listed for GPIO pins is the pin's set output level) and it may not show the
    /// current direction or set output level of a GPIO pin.
    ///
    /// </div>
    ///
    /// # Datasheet
    ///
    /// See section 3.1.12 for the underlying Get GPIO Values HID command.
    pub fn gpio_read(&self) -> Result<GpioValues, Error> {
        let buf = self
            .transfer(&UsbReport::new(McpCommand::GetGpioValues))?
            .expect("Always has response buffer.");
        Ok(GpioValues::from_buffer(&buf))
    }

    /// Change GPIO pins' direction and output logic level.
    ///
    /// You should prefer this method over [`MCP2221::sram_write_settings`] to change
    /// GPIO pin direction or output level, and use that method for altering the pin
    /// function (eg, GPIO, ADC, etc). Changing GP pin settings with that method
    /// requires an additional read command to the device to work around a firmware bug
    /// that resets analog voltage references (see the note in section 1.8.1.1 of the
    /// datasheet from revision D onwards).
    ///
    /// Note that this method will not change the mode of GP pins that are not set for
    /// GPIO operation. That must be done first by setting the pin mode, either
    /// temporarily via [`MCP2221::sram_write_settings`], or persistently via
    /// [`MCP2221::flash_write_gp_settings`] (and then resetting the device).
    ///
    /// The ability to set a pin as an input while also setting its output logic level
    /// reflects the structure of the underlying MCP2221 command but is otherwise
    /// meaningless.
    ///
    /// <div class="warning">
    ///
    /// Using this method will mean that the SRAM settings (as read through
    /// [`MCP2221::sram_read_settings`]) will not reflect the current GPIO pin direction
    /// and output level. This appears to be a bug in the MCP2221 firmware and is not
    /// documented in the datasheet.
    ///
    /// </div>
    ///
    /// # Datasheet
    ///
    /// See section 3.1.11 of the datasheet for the underlying Set GPIO Output Values
    /// HID command.
    pub fn gpio_write(&self, changes: &GpioChanges) -> Result<(), Error> {
        let mut command = UsbReport::new(McpCommand::SetGpioOutputValues);
        changes.apply_to_buffer(&mut command.write_buffer);
        self.transfer(&command)?;
        Ok(())
    }

    ////////////////////////////////////////////////////////////////////////////////
    // Analog
    ////////////////////////////////////////////////////////////////////////////////

    /// Read the current values of the three-channel ADC.
    ///
    /// Pins GP1, GP2, and GP3 are connected to separate channels of the ADC, and the
    /// return value will contain the analog reading for each if that pin is configured
    /// as an analog input. The current ADC voltage reference is included so that you
    /// may convert a 10-bit reading to a voltage (`reading / 1023 * Vref`).
    ///
    /// # Internals
    ///
    /// The ADC readings are reported in the [`Status`] structure and are always
    /// available. In practice, these readings are what you'd expect no matter the
    /// set mode of the pin (GPIO output low is 0, and high 1023, for example).
    /// However, the datasheet makes no claims about behaviour in this state, so
    /// it's officially undefined and unsupported.
    ///
    /// [`Status`]: crate::status::Status
    ///
    /// # Datasheet
    ///
    /// See section 1.8.2 for information about the 10-bit ADC and section 3.1.1 for
    /// the underlying Status/Set Parameters HID command.
    pub fn analog_read(&self) -> Result<AdcReading, Error> {
        let raw = self.status()?.adc_values;
        let (chip_settings, gp) = self.sram_read_settings()?;
        let reading = AdcReading {
            vref: chip_settings.adc_reference,
            gp1: gp.gp1_mode.is_analog_input().then_some(raw.ch1),
            gp2: gp.gp2_mode.is_analog_input().then_some(raw.ch2),
            gp3: gp.gp3_mode.is_analog_input().then_some(raw.ch3),
        };
        Ok(reading)
    }

    /// Perform an analog write to the DAC.
    ///
    /// This writes a 5-bit value to the MCP2221’s digital-to-analog converter, which
    /// outputs a corresponding voltage on appropriately configured pins. GP2 and GP3
    /// can be used for analog output pins, though they share the single DAC and will
    /// have the same voltage.
    ///
    /// Values above the 5-bit range of the DAC (`0..=31`) are clamped to the
    /// maximum value of 31.
    ///
    /// Note that the DAC output is not linear from 0V to the reference and (at least
    /// with 3.3V supply) does not reach the reference voltage. This is detailed in
    /// the crate readme.
    ///
    /// This setting is not persisted across reset. See [`MCP2221::flash_write_chip_settings`]
    /// to set the DAC to output a particular value at power-on.
    ///
    /// # Datasheet
    ///
    /// See section 1.8.3 for information about the 5-bit DAC, and section 3.1.13 for
    /// the underlying Set SRAM Settings HID command.
    pub fn analog_write(&self, value: u8) -> Result<(), Error> {
        // with_dac_value limits the value to 31.
        self.sram_write_settings(SramSettingsChanges::new().with_dac_value(value))?;
        Ok(())
    }

    /// Configure the ADC voltage reference in SRAM.
    ///
    /// This will alter the current behaviour of the MCP2221 but will not persist
    /// across device reset.
    ///
    /// Unlike with the DAC, setting the ADC reference to Vrm with a level of "off"
    /// results in a reference that seems to be equivalent to Vdd (as the datasheet
    /// suggests).
    ///
    /// See the [`analog`](crate::analog) module for more information about the
    /// voltage references.
    ///
    /// # Datasheet
    ///
    /// See section 1.8.2 for information about the 10-bit ADC, section 1.8.1.1 for
    /// details about Vrm, and section 3.1.13 for the underlying Set SRAM Settings
    /// HID command.
    pub fn analog_set_input_reference(&self, source: VoltageReference) -> Result<(), Error> {
        self.sram_write_settings(SramSettingsChanges::new().with_adc_reference(source))?;
        Ok(())
    }

    /// Configure the DAC voltage reference in SRAM.
    ///
    /// This will alter the current behaviour of the MCP2221 but will not persist
    /// across device reset.
    ///
    /// <div class="warning">
    ///
    /// Setting the DAC reference to Vrm with a level of "off" will cause the output
    /// voltage to be just above 0V at all output values. The datasheet suggests (in
    /// section 1.8.1.1) that "off" means that Vrm will reference Vdd (the supply
    /// voltage). This is true for the ADC but _not_ the DAC. Just use Vdd instead.
    ///
    /// </div>
    ///
    /// See the [`analog`](crate::analog) module for more information about the
    /// voltage references.
    ///
    /// # Datasheet
    ///
    /// See section 1.8.3 for information about the 5-bit DAC, section 1.8.1.1 for
    /// details about Vrm (with the caveat listed above), and section 3.1.13 for
    /// the underlying Set SRAM Settings HID command.
    pub fn analog_set_output_reference(&self, source: VoltageReference) -> Result<(), Error> {
        self.sram_write_settings(SramSettingsChanges::new().with_dac_reference(source))?;
        Ok(())
    }

    ////////////////////////////////////////////////////////////////////////////////
    // Interrupt flag management
    ////////////////////////////////////////////////////////////////////////////////

    /// Read the edge-triggered interrupt flag.
    ///
    /// GP1 can be configured to detect external interrupts on rising or falling edges.
    /// If so, the interrupt flag will be set when an edge is detected.
    ///
    /// ## Datasheet
    ///
    /// There is very little about interrupt detection in the MCP2221 datasheet. See
    /// byte 6 in table 3-36 for descriptions of the related settings. See section
    /// 1.10 for a very brief general overview.
    pub fn interrupt_detected(&self) -> Result<bool, Error> {
        self.status().map(|s| s.interrupt_detected)
    }

    /// Clear the edge-triggered interrupt flag.
    ///
    /// The flag indicates that an edge has been detected on GP1, when GP1 is in
    /// interrupt-detection mode. The interrupt detection conditions (positive edge,
    /// negative edge, or both) are not changed.
    ///
    /// ## Datasheet
    ///
    /// There is very little about interrupt detection in the MCP2221 datasheet. See
    /// byte 6 in table 3-36 for descriptions of the related settings. See section
    /// 1.10 for a very brief general overview.
    pub fn interrupt_clear(&self) -> Result<(), Error> {
        self.sram_write_settings(
            SramSettingsChanges::new()
                .with_interrupt_settings(InterruptSettingsChanges::clear_flag(true)),
        )
    }

    ////////////////////////////////////////////////////////////////////////////////
    // SRAM settings
    ////////////////////////////////////////////////////////////////////////////////

    /// Retrieve the chip and GP pin settings stored in SRAM.
    ///
    /// <div class="warning">
    ///
    /// Do not rely on the returned settings accurately reflecting the current
    /// state of the MCP2221. Some commands will (in practice) change these settings
    /// without those changes being shown when subsequently reading the SRAM.
    ///
    /// - GPIO pin direction and level after using the Set GPIO Output Values HID
    ///   command (implemented by [`MCP2221::gpio_write`]).
    /// - Vrm reference level set to "off" after setting GP pin settings via the Set
    ///   SRAM Settings HID command (implemented by [`MCP2221::sram_write_settings`])
    ///   _without_ also explicitly setting the Vrm level. See the note in section
    ///   1.8.1.1 of the datasheet, as well as the documentation for
    ///   [`SramSettingsChanges::with_gp_modes`].
    ///
    /// </div>
    ///
    /// ## Datasheet
    ///
    /// See section 3.1.14 of the datasheet for details about the underlying Get SRAM
    /// Settings HID command, and section 1.4 for information about the configuration
    /// process at power-up.
    pub fn sram_read_settings(&self) -> Result<(ChipSettings, GpSettings), Error> {
        let command = UsbReport::new(McpCommand::GetSRAMSettings);
        let buf = self
            .transfer(&command)?
            .expect("Always has response buffer.");
        Ok((
            ChipSettings::from_buffer(&buf),
            GpSettings::try_from_sram_buffer(&buf)?,
        ))
    }

    /// Change run-time chip and GP pin settings.
    ///
    /// This will alter the current behaviour of the MCP2221 but will not persist
    /// across device reset. Note that only a subset of the settings read from SRAM
    /// can be changed.
    ///
    /// If you only need to change GPIO pin direction or output level, you should
    /// prefer to use [`MCP2221::gpio_write`].
    ///
    /// <div class="warning">
    ///
    /// Changing the GP pin settings without also setting Vrm levels for the ADC and
    /// DAC will result in the Vrm level for each being reset to "off". This appears
    /// to be a MCP2221 firmware bug and is noted in section 1.8.1.1 of the datasheet.
    ///
    /// </div>
    ///
    /// # Datasheet
    ///
    /// See section 3.1.13 of the datasheet for details about the underlying Set SRAM
    /// Settings HID command.
    pub fn sram_write_settings(&self, settings: &SramSettingsChanges) -> Result<(), Error> {
        let mut command = UsbReport::new(McpCommand::SetSRAMSettings);
        settings.apply_to_sram_buffer(&mut command.write_buffer);
        self.transfer(&command)?;
        Ok(())
    }

    /// Change the GP pin settings in SRAM while preserving the ADC and DAC references.
    ///
    /// This is a convenience wrapper around [`Self::sram_write_settings`] that does
    /// the work of reading the current ADC & DAC voltage references and re-writing
    /// them, to avoid the Vrm reset bug.
    pub fn sram_write_gp_settings(&self, gp_settings: GpSettings) -> Result<(), Error> {
        let (chip_settings, _) = self.sram_read_settings()?;
        self.sram_write_settings(SramSettingsChanges::new().with_gp_modes(
            gp_settings,
            Some(chip_settings.dac_reference),
            Some(chip_settings.adc_reference),
        ))
    }

    ////////////////////////////////////////////////////////////////////////////////
    // Flash settings
    ////////////////////////////////////////////////////////////////////////////////

    /// Read chip settings from flash memory.
    ///
    /// The chip settings collect several important but unrelated configuration options.
    /// See the fields of [`ChipSettings`] and table 3-15 of the datasheet for details
    /// about each one.
    ///
    /// Settings in flash memory take effect on power-up.
    ///
    /// # Datasheet
    ///
    /// See section 1.4 for information on the configuration process. See section
    /// 3.1.2 for the underlying Read Flash Data HID command and table 3-5 for the
    /// relevant subcommand.
    pub fn flash_read_chip_settings(&self) -> Result<ChipSettings, Error> {
        let command = McpCommand::ReadFlashData(FlashDataSubCode::ChipSettings);
        let buf = self
            .transfer(&UsbReport::new(command))?
            .expect("Always has response buffer.");
        Ok(ChipSettings::from_buffer(&buf))
    }

    /// Write chip settings to flash memory.
    ///
    /// The chip settings collect several important but unrelated configuration options.
    /// See the fields of [`ChipSettings`] and table 3-12 of the datasheet for details
    /// about each one.
    ///
    /// Settings stored in the flash memory of the MCP2221 take effect when the device
    /// is powered-up.
    ///
    /// # Datasheet
    ///
    /// See section 1.4 for information on the configuration process. See section
    /// 3.1.3 for the underlying Write Flash Data HID command and table 3-12 for the
    /// relevant subcommand.
    pub fn flash_write_chip_settings(&self, cs: ChipSettings) -> Result<(), Error> {
        let mut command =
            UsbReport::new(McpCommand::WriteFlashData(FlashDataSubCode::ChipSettings));
        cs.apply_to_flash_buffer(&mut command.write_buffer);
        self.transfer(&command)?;
        Ok(())
    }

    /// Read GP pin settings from flash memory.
    ///
    /// These are the initial settings for the GP pins when the device is powered-up.
    ///
    /// Settings in flash memory take effect on power-up.
    ///
    /// # Datasheet
    ///
    /// See section 1.4 for information on the configuration process. See section
    /// 3.1.2 for the underlying Read Flash Data HID command and table 3-6 for the
    /// relevant subcommand.
    pub fn flash_read_gp_settings(&self) -> Result<GpSettings, Error> {
        let command = McpCommand::ReadFlashData(FlashDataSubCode::GPSettings);
        let buf = self
            .transfer(&UsbReport::new(command))?
            .expect("Always has response buffer.");
        GpSettings::try_from_flash_buffer(&buf)
    }

    /// Write GP pin settings to flash memory.
    ///
    /// This can be used to set appropriate defaults for the pin functions for your
    /// use case, and further (temporary) changes can be made at run time via the
    /// methods [`MCP2221::sram_write_settings`] (for changing pin functions) and
    /// [`MCP2221::gpio_write`] (for changing digital output direction and level).
    ///
    /// Settings stored in the flash memory of the MCP2221 take effect when the device
    /// is powered-up.
    ///
    /// # Datasheet
    ///
    /// See section 1.4 for information on the configuration process. See section
    /// 3.1.3 for the underlying Write Flash Data HID command and table 3-13 for
    /// the relevant subcommand.
    pub fn flash_write_gp_settings(&self, gp: GpSettings) -> Result<(), Error> {
        let mut command = UsbReport::new(McpCommand::WriteFlashData(FlashDataSubCode::GPSettings));
        gp.apply_to_flash_buffer(&mut command.write_buffer);
        self.transfer(&command)?;
        Ok(())
    }

    /// Read the USB manufacturer descriptor string from flash memory.
    ///
    /// The manufacturer descriptor string is used to identify a device to a
    /// USB host.
    ///
    /// If you wish to read the USB vendor ID number (VID), see
    /// [`MCP2221::flash_read_chip_settings`].
    ///
    /// # Datasheet
    ///
    /// See section 3.1.2 for the underlying Read Flash Data HID command, and
    /// table 3-7 for the relevant subcommand.
    pub fn usb_manufacturer(&self) -> Result<DeviceString, Error> {
        let command = McpCommand::ReadFlashData(FlashDataSubCode::UsbManufacturerDescriptor);
        let buf = self
            .transfer(&UsbReport::new(command))?
            .expect("Always has response buffer.");
        DeviceString::try_from_buffer(&buf)
    }

    /// Change the USB manufacturer descriptor string.
    ///
    /// The manufacturer descriptor string is used to identify a device to a
    /// USB host. This setting is stored in flash, so the MCP2221 will have to
    /// be reset (and re-enumerate) for the change to take effect.
    ///
    /// The manufacturer string can be at most 30 UTF-16 code points long.
    ///
    /// If you wish to change the USB vendor ID number (VID), see
    /// [`MCP2221::flash_write_chip_settings`].
    ///
    /// # Datasheet
    ///
    /// See section 3.1.3 for the underlying Write Flash Data HID command, and
    /// table 3-14 for the relevant subcommand.
    pub fn usb_change_manufacturer(&self, s: &DeviceString) -> Result<(), Error> {
        let mut command = UsbReport::new(McpCommand::WriteFlashData(
            FlashDataSubCode::UsbManufacturerDescriptor,
        ));
        s.apply_to_flash_buffer(&mut command.write_buffer);
        self.transfer(&command)?;
        Ok(())
    }

    /// Read the USB product descriptor string from flash memory.
    ///
    /// The product descriptor string is used to identify a device to a USB host.
    ///
    /// If you wish to read the USB product ID number (VID), see
    /// [`MCP2221::flash_read_chip_settings`].
    ///
    /// # Datasheet
    ///
    /// See section 3.1.2 for the underlying Read Flash Data HID command, and
    /// table 3-8 for the relevant subcommand.
    pub fn usb_product(&self) -> Result<DeviceString, Error> {
        let command = McpCommand::ReadFlashData(FlashDataSubCode::UsbProductDescriptor);
        let buf = self
            .transfer(&UsbReport::new(command))?
            .expect("Always has response buffer.");
        DeviceString::try_from_buffer(&buf)
    }

    /// Change the USB product descriptor string.
    ///
    /// The product descriptor string is used to identify a device to a USB host.
    /// This setting is stored in flash, so the MCP2221 will have to be reset
    /// (and re-enumerate) for the change to take effect.
    ///
    /// The product string can be at most 30 UTF-16 code points long.
    ///
    /// If you wish to change the USB product ID number (PID), see
    /// [`MCP2221::flash_write_chip_settings`].
    ///
    /// # Datasheet
    ///
    /// See section 3.1.3 for the underlying Write Flash Data HID command, and
    /// table 3-15 for the relevant subcommand.
    pub fn usb_change_product(&self, s: &DeviceString) -> Result<(), Error> {
        let mut command = UsbReport::new(McpCommand::WriteFlashData(
            FlashDataSubCode::UsbProductDescriptor,
        ));
        s.apply_to_flash_buffer(&mut command.write_buffer);
        self.transfer(&command)?;
        Ok(())
    }

    /// Read the USB serial number descriptor string from flash memory.
    ///
    /// The serial number descriptor string is used to identify a device to a USB host.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.2 for the underlying Read Flash Data HID command, and
    /// table 3-9 for the relevant subcommand.
    pub fn usb_serial_number(&self) -> Result<DeviceString, Error> {
        let command = McpCommand::ReadFlashData(FlashDataSubCode::UsbSerialNumberDescriptor);
        let buf = self
            .transfer(&UsbReport::new(command))?
            .expect("Always has response buffer.");
        DeviceString::try_from_buffer(&buf)
    }

    /// Change the USB serial number descriptor string.
    ///
    /// The serial number descriptor string is used to identify a device to a USB host.
    /// This setting is stored in flash, so the MCP2221 will have to be reset (and
    /// re-enumerate) for the change to take effect.
    ///
    /// The serial number string can be at most 30 UTF-16 code points long.
    ///
    /// # Datasheet
    ///
    /// See section 3.1.3 for the underlying Write Flash Data HID command, and
    /// table 3-16 for the relevant subcommand.
    pub fn usb_change_serial_number(&self, s: &DeviceString) -> Result<(), Error> {
        let mut command = UsbReport::new(McpCommand::WriteFlashData(
            FlashDataSubCode::UsbSerialNumberDescriptor,
        ));
        s.apply_to_flash_buffer(&mut command.write_buffer);
        self.transfer(&command)?;
        Ok(())
    }

    /// Read chip factory serial number.
    ///
    /// Read the factory-set device serial number. For the MCP2221A, this appears to
    /// always be "01234567" in ASCII. It cannot be changed.
    ///
    /// This function uses [`String::from_utf8_lossy`], so if you read a serial number
    /// with Unicode replacement characters, your device has an unexpected, non-ASCII
    /// factory serial number and you should [file an issue].
    ///
    /// [file an issue]: https://github.com/robjwells/mcp2221-hal/issues
    ///
    /// # Datasheet
    ///
    /// See section 3.1.2 for the underlying Read Flash Data HID command, and
    /// table 3-10 for the relevant subcommand.
    pub fn factory_serial_number(&self) -> Result<String, Error> {
        let command = McpCommand::ReadChipFactorySerialNumber;
        let buf = self
            .transfer(&UsbReport::new(command))?
            .expect("Always has response buffer.");
        let length = buf[2] as usize;
        let serial_number_portion = &buf[4..(4 + length)];
        Ok(String::from_utf8_lossy(serial_number_portion).into())
    }

    ////////////////////////////////////////////////////////////////////////////////
    // USB - miscellaneous
    ////////////////////////////////////////////////////////////////////////////////

    /// Get the USB HID device information from the host's USB interface.
    ///
    /// This is a thin wrapper around [`HidDevice::get_device_info`].
    ///
    /// # Errors
    ///
    /// An error will be returned if the device information cannot be obtained from the
    /// underlying USB interface.
    pub fn usb_device_info(&self) -> Result<hidapi::DeviceInfo, Error> {
        self.inner.get_device_info().map_err(Error::from)
    }
}