mxdbg 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firmware/bootloader.bin +0 -0
- firmware/mxdbg.bin +0 -0
- firmware/partition-table.bin +0 -0
- mxdbg-0.2.2.dist-info/METADATA +30 -0
- mxdbg-0.2.2.dist-info/RECORD +12 -0
- mxdbg-0.2.2.dist-info/WHEEL +5 -0
- mxdbg-0.2.2.dist-info/licenses/LICENSE +202 -0
- mxdbg-0.2.2.dist-info/top_level.txt +3 -0
- mxdbg.py +2050 -0
- mxdbg.toml +197 -0
- mxdbg_ui.py +262 -0
- paw3311dw.py +110 -0
mxdbg.py
ADDED
|
@@ -0,0 +1,2050 @@
|
|
|
1
|
+
"""
|
|
2
|
+
* Copyright (c) 2024 MixoSense Technology Ltd <contact@mixosense.com>.
|
|
3
|
+
*
|
|
4
|
+
* All rights are reserved.
|
|
5
|
+
* Proprietary and confidential.
|
|
6
|
+
* Unauthorized copying of this file, via any medium is strictly prohibited.
|
|
7
|
+
* Any use is subject to an appropriate license granted by MixoSense Technology
|
|
8
|
+
* Ltd.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ctypes
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from types import MappingProxyType
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from loguru import logger
|
|
21
|
+
except ImportError:
|
|
22
|
+
print("Please install loguru: pip install loguru")
|
|
23
|
+
raise ImportError
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import toml
|
|
27
|
+
except ImportError:
|
|
28
|
+
print("Please install toml: pip install toml")
|
|
29
|
+
raise ImportError
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import serial.tools.list_ports
|
|
33
|
+
from serial import Serial
|
|
34
|
+
except ImportError:
|
|
35
|
+
print("Please install pyserial: pip install pyserial")
|
|
36
|
+
raise ImportError
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MXDBG:
|
|
40
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
41
|
+
self._client = None
|
|
42
|
+
self.__pwm_states = [False, False, False]
|
|
43
|
+
self.__crc_enable = True
|
|
44
|
+
self.__mxdbg_header_file = None
|
|
45
|
+
self.__mxdbg_toml_path = None
|
|
46
|
+
|
|
47
|
+
self.version, self.task_cmd, self.__error_map = self.__parse_and_map()
|
|
48
|
+
|
|
49
|
+
__constants_gpio_mode__ = {
|
|
50
|
+
"GPIO_MODE_DISABLE": 0,
|
|
51
|
+
"GPIO_MODE_INPUT": 1,
|
|
52
|
+
"GPIO_MODE_OUTPUT": 2,
|
|
53
|
+
"GPIO_MODE_OUTPUT_OD": 6,
|
|
54
|
+
"GPIO_MODE_INPUT_OUTPUT_OD": 7,
|
|
55
|
+
"GPIO_MODE_INPUT_OUTPUT": 3,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
self.gpio_mode = MappingProxyType(__constants_gpio_mode__)
|
|
59
|
+
|
|
60
|
+
__constants_usb_info__ = {
|
|
61
|
+
"USB_VID": 0x303A,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
self.usb_info = MappingProxyType(__constants_usb_info__)
|
|
65
|
+
|
|
66
|
+
__constants_spi_common_bus_flag__ = {
|
|
67
|
+
"SPICOMMON_BUSFLAG_SLAVE": 0, # Initialize I/O in slave mode
|
|
68
|
+
"SPICOMMON_BUSFLAG_MASTER": 1 << 0, # Initialize I/O in master mode
|
|
69
|
+
"SPICOMMON_BUSFLAG_IOMUX_PINS": 1 << 1, # Check using iomux pins.
|
|
70
|
+
# Or indicates the pins are configured through the IO mux rather than GPIO matrix.
|
|
71
|
+
"SPICOMMON_BUSFLAG_GPIO_PINS": 1
|
|
72
|
+
<< 2, # Force the signals to be routed through GPIO matrix.
|
|
73
|
+
"SPICOMMON_BUSFLAG_SCLK": 1
|
|
74
|
+
<< 3, # Check existing of SCLK pin. Or indicates CLK line initialized.
|
|
75
|
+
"SPICOMMON_BUSFLAG_MISO": 1
|
|
76
|
+
<< 4, # Check existing of MISO pin. Or indicates MISO line initialized.
|
|
77
|
+
"SPICOMMON_BUSFLAG_MOSI": 1
|
|
78
|
+
<< 5, # Check existing of MOSI pin. Or indicates MOSI line initialized.
|
|
79
|
+
# Check MOSI and MISO pins can output. Or indicates bus able to work under DIO mode.
|
|
80
|
+
"SPICOMMON_BUSFLAG_DUAL": 1 << 6,
|
|
81
|
+
# Check existing of WP and HD pins. Or indicates WP & HD pins initialized.
|
|
82
|
+
"SPICOMMON_BUSFLAG_WPHD": 1 << 7,
|
|
83
|
+
# Check existing of MOSI and MISO pins as output. Or indicates bus able to work under QIO mode.
|
|
84
|
+
# Check existing of MOSI/MISO/WP/HD pins as output. Or indicates bus able to work under QIO mode.
|
|
85
|
+
"SPICOMMON_BUSFLAG_QUAD": 1 << 6 | 1 << 7,
|
|
86
|
+
# Check existing of IO4~IO7 pins. Or indicates IO4~IO7 pins initialized.
|
|
87
|
+
"SPICOMMON_BUSFLAG_IO4_IO7": 1 << 8,
|
|
88
|
+
# Check existing of MOSI/MISO/WP/HD/SPIIO4/SPIIO5/SPIIO6/SPIIO7 pins as output. Or indicates bus able to work under octal mode.
|
|
89
|
+
"SPICOMMON_BUSFLAG_OCTAL": 1 << 6 | 1 << 7 | 1 << 8,
|
|
90
|
+
"SPICOMMON_BUSFLAG_NATIVE_PINS": 1 << 1, # Check using iomux pins.
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
self.spi_common_bus_flag = MappingProxyType(__constants_spi_common_bus_flag__)
|
|
94
|
+
|
|
95
|
+
__constants_spi_device__ = {
|
|
96
|
+
"SPI_DEVICE_NONE": 0,
|
|
97
|
+
"SPI_DEVICE_TXBIT_LSBFIRST": 1
|
|
98
|
+
<< 0, # Transmit command/address/data LSB first instead of the default MSB first
|
|
99
|
+
"SPI_DEVICE_RXBIT_LSBFIRST": 1
|
|
100
|
+
<< 1, # Receive data LSB first instead of the default MSB first
|
|
101
|
+
"SPI_DEVICE_BIT_LSBFIRST": 1 << 0
|
|
102
|
+
| 1 << 1, # Transmit and receive LSB first
|
|
103
|
+
"SPI_DEVICE_3WIRE": 1
|
|
104
|
+
<< 2, # Use MOSI (=spid) for both sending and receiving data
|
|
105
|
+
"SPI_DEVICE_POSITIVE_CS": 1
|
|
106
|
+
<< 3, # Make CS positive during a transaction instead of negative
|
|
107
|
+
"SPI_DEVICE_HALFDUPLEX": 1
|
|
108
|
+
<< 4, # Transmit data before receiving it, instead of simultaneously
|
|
109
|
+
"SPI_DEVICE_CLK_AS_CS": 1 << 5, # Output clock on CS line if CS is active
|
|
110
|
+
# There are timing issue when reading at high frequency (the frequency is related to whether iomux pins are used, valid time after slave sees the clock).
|
|
111
|
+
"SPI_DEVICE_NO_DUMMY": 1 << 6,
|
|
112
|
+
# In half-duplex mode, the driver automatically inserts dummy bits before reading phase to fix the timing issue. Set this flag to disable this feature.
|
|
113
|
+
"SPI_DEVICE_DDRCLK": 1 << 7,
|
|
114
|
+
# In full-duplex mode, however, the hardware cannot use dummy bits, so there is no way to prevent data being read from getting corrupted.
|
|
115
|
+
"SPI_DEVICE_NO_RETURN_RESULT": 1 << 8,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
self.spi_device = MappingProxyType(__constants_spi_device__)
|
|
119
|
+
|
|
120
|
+
self.__all_valid_pins = [
|
|
121
|
+
-1,
|
|
122
|
+
1,
|
|
123
|
+
2,
|
|
124
|
+
4,
|
|
125
|
+
5,
|
|
126
|
+
6,
|
|
127
|
+
7,
|
|
128
|
+
8,
|
|
129
|
+
9,
|
|
130
|
+
10,
|
|
131
|
+
11,
|
|
132
|
+
12,
|
|
133
|
+
13,
|
|
134
|
+
14,
|
|
135
|
+
15,
|
|
136
|
+
16,
|
|
137
|
+
17,
|
|
138
|
+
18,
|
|
139
|
+
33,
|
|
140
|
+
34,
|
|
141
|
+
35,
|
|
142
|
+
36,
|
|
143
|
+
37,
|
|
144
|
+
38,
|
|
145
|
+
39,
|
|
146
|
+
40,
|
|
147
|
+
41,
|
|
148
|
+
42,
|
|
149
|
+
]
|
|
150
|
+
self.__pwm_used_pins = [16, 17, 18]
|
|
151
|
+
self.__i2c_used_pins = [10, 11]
|
|
152
|
+
self.__spi_used_pins = [12, 13, 14, 15]
|
|
153
|
+
self.__gpio_used_pins = set()
|
|
154
|
+
|
|
155
|
+
self.__gpio_valid_pins = None
|
|
156
|
+
self.__i2c_valid_pins = None
|
|
157
|
+
self.__spi_valid_pins = None
|
|
158
|
+
self.__pwm_valid_pins = None
|
|
159
|
+
|
|
160
|
+
self.__pca9557pw_addr = 0x18
|
|
161
|
+
self.__tca9555pwr_addr = 0x20
|
|
162
|
+
self.__extboard_version = None
|
|
163
|
+
|
|
164
|
+
self.__expand_io_init_status = False
|
|
165
|
+
self.__power_init_status = False
|
|
166
|
+
self.__expand_io_mode_bitmask = 0x0000
|
|
167
|
+
|
|
168
|
+
# TODO: auto-connect should be made optional
|
|
169
|
+
self.connect(**kwargs)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def pwm_valid_pins(self) -> list:
|
|
173
|
+
if self.__pwm_valid_pins is None:
|
|
174
|
+
self.__pwm_valid_pins = list(
|
|
175
|
+
set(self.__all_valid_pins)
|
|
176
|
+
- set(self.__gpio_used_pins)
|
|
177
|
+
- set(self.__spi_used_pins)
|
|
178
|
+
- set(self.__i2c_used_pins)
|
|
179
|
+
)
|
|
180
|
+
return self.__pwm_valid_pins
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def i2c_valid_pins(self) -> list:
|
|
184
|
+
if self.__i2c_valid_pins is None:
|
|
185
|
+
self.__i2c_valid_pins = list(
|
|
186
|
+
set(self.__all_valid_pins)
|
|
187
|
+
- set(self.__pwm_used_pins)
|
|
188
|
+
- set(self.__spi_used_pins)
|
|
189
|
+
- set(self.__gpio_used_pins)
|
|
190
|
+
)
|
|
191
|
+
return self.__i2c_valid_pins
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def spi_valid_pins(self) -> list:
|
|
195
|
+
if self.__spi_valid_pins is None:
|
|
196
|
+
self.__spi_valid_pins = list(
|
|
197
|
+
set(self.__all_valid_pins)
|
|
198
|
+
- set(self.__pwm_used_pins)
|
|
199
|
+
- set(self.__i2c_used_pins)
|
|
200
|
+
- set(self.__gpio_used_pins)
|
|
201
|
+
)
|
|
202
|
+
return self.__spi_valid_pins
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def gpio_valid_pins(self) -> list:
|
|
206
|
+
if self.__gpio_valid_pins is None:
|
|
207
|
+
self.__gpio_valid_pins = list(
|
|
208
|
+
set(self.__all_valid_pins)
|
|
209
|
+
- set(self.__pwm_used_pins)
|
|
210
|
+
- set(self.__spi_used_pins)
|
|
211
|
+
- set(self.__i2c_used_pins)
|
|
212
|
+
)
|
|
213
|
+
return self.__gpio_valid_pins
|
|
214
|
+
|
|
215
|
+
def __mark_pwm_used(self, pin) -> None:
|
|
216
|
+
|
|
217
|
+
if pin == -1:
|
|
218
|
+
logger.warning("PWM is not supported to set pin to -1.")
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
if pin in self.pwm_valid_pins:
|
|
222
|
+
self.__pwm_used_pins.append(pin)
|
|
223
|
+
self.__pwm_valid_pins = None
|
|
224
|
+
else:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"Pin {pin} is not a valid PWM pin or already used by other module."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def __mark_i2c_used(self, pin) -> None:
|
|
230
|
+
|
|
231
|
+
if pin == -1:
|
|
232
|
+
logger.warning("I2C is not supported to set pin to -1.")
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
if pin in self.i2c_valid_pins:
|
|
236
|
+
self.__i2c_used_pins.append(pin)
|
|
237
|
+
self.__i2c_valid_pins = None
|
|
238
|
+
else:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"Pin {pin} is not a valid I2C pin or already used by other module."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def __mark_spi_used(self, pin) -> None:
|
|
244
|
+
|
|
245
|
+
if pin == -1:
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
if pin in self.spi_valid_pins:
|
|
249
|
+
self.__spi_used_pins.append(pin)
|
|
250
|
+
self.__spi_valid_pins = None
|
|
251
|
+
else:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
f"Pin {pin} is not a valid SPI pin or already used by other module."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def __mark_gpio_used(self, pin) -> None:
|
|
257
|
+
|
|
258
|
+
if pin == -1:
|
|
259
|
+
logger.warning("GPIO is not supported to set pin to -1.")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
if pin in self.gpio_valid_pins:
|
|
263
|
+
self.__gpio_used_pins.add(pin)
|
|
264
|
+
self.__gpio_valid_pins = None
|
|
265
|
+
else:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f"Pin {pin} is not a valid GPIO pin or already used by other module."
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def mark_pin_free(self, pin) -> None:
|
|
271
|
+
"""
|
|
272
|
+
@brief check all the used pins if the pin was in it, then remove it.
|
|
273
|
+
@param pin: pin number. `-1` is not supported.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
if pin == -1:
|
|
277
|
+
logger.warning("Pin is not supported to be set as -1.")
|
|
278
|
+
raise ValueError("Pin is not supported to be set as -1.")
|
|
279
|
+
|
|
280
|
+
if pin in self.__gpio_used_pins:
|
|
281
|
+
self.__gpio_used_pins.remove(pin)
|
|
282
|
+
self.__gpio_valid_pins = None
|
|
283
|
+
elif pin in self.__spi_used_pins:
|
|
284
|
+
self.__spi_used_pins.remove(pin)
|
|
285
|
+
self.__spi_valid_pins = None
|
|
286
|
+
elif pin in self.__i2c_used_pins:
|
|
287
|
+
self.__i2c_used_pins.remove(pin)
|
|
288
|
+
self.__i2c_valid_pins = None
|
|
289
|
+
elif pin in self.__pwm_used_pins:
|
|
290
|
+
self.__pwm_used_pins.remove(pin)
|
|
291
|
+
self.__pwm_valid_pins = None
|
|
292
|
+
else:
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"Pin {pin} is not used by any module. Or it is not a valid pin."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def __find_ports(self) -> list[str]:
|
|
298
|
+
"""按 USB_VID 探测所有 ESP32 串口(如 ['/dev/ttyACM0']),无则返回空列表。"""
|
|
299
|
+
ports = []
|
|
300
|
+
for port in sorted(serial.tools.list_ports.comports()):
|
|
301
|
+
if port.vid is None or port.pid is None:
|
|
302
|
+
continue
|
|
303
|
+
if self.usb_info["USB_VID"] == port.vid:
|
|
304
|
+
ports.append(port.name)
|
|
305
|
+
return ports
|
|
306
|
+
|
|
307
|
+
def __find_port(self) -> str | None:
|
|
308
|
+
"""按 USB_VID 探测 ESP32 串口,取第一个;未找到返回 None。"""
|
|
309
|
+
ports = self.__find_ports()
|
|
310
|
+
return ports[0] if ports else None
|
|
311
|
+
|
|
312
|
+
def connect(self, port: str | None = None, **kwargs) -> None:
|
|
313
|
+
"""连接 ESP32。
|
|
314
|
+
|
|
315
|
+
@param port: 串口(如 '/dev/ttyACM0' / 'COM3');None 时按 USB_VID=0x303A 自动探测。
|
|
316
|
+
显式指定端口可支持 CH340/FT232/CP2102 等 USB 转串口方案及多设备场景。
|
|
317
|
+
"""
|
|
318
|
+
_port = port
|
|
319
|
+
retry_times = 10
|
|
320
|
+
while True:
|
|
321
|
+
if _port is None:
|
|
322
|
+
_port = self.__find_port()
|
|
323
|
+
|
|
324
|
+
if _port is not None:
|
|
325
|
+
break
|
|
326
|
+
else:
|
|
327
|
+
retry_times -= 1
|
|
328
|
+
time.sleep(1)
|
|
329
|
+
|
|
330
|
+
if retry_times == 0:
|
|
331
|
+
raise ValueError("No device found.")
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
if sys.platform != "win32" and not _port.startswith("/"):
|
|
335
|
+
_port = f"/dev/{_port}"
|
|
336
|
+
|
|
337
|
+
# use serial port to connect
|
|
338
|
+
self._client = Serial(_port, 115200, timeout=5, write_timeout=1, **kwargs)
|
|
339
|
+
logger.info(f"Using serial port to connect. Port: {_port}")
|
|
340
|
+
|
|
341
|
+
except Exception as e:
|
|
342
|
+
raise ValueError(f"Failed to connect: {e}")
|
|
343
|
+
|
|
344
|
+
logger.info(
|
|
345
|
+
"Using ESP32-S3R8. Software version: v{}.{}".format(
|
|
346
|
+
self.version["MAJOR"], self.version["MINOR"]
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def disconnect(self) -> None:
|
|
351
|
+
self._client.close()
|
|
352
|
+
|
|
353
|
+
def flash_firmware(
|
|
354
|
+
self, port: str | None = None, confirm: bool = True
|
|
355
|
+
) -> tuple[bool, str]:
|
|
356
|
+
"""烧录包内固件到 ESP32(无需本地安装 ESP-IDF)。
|
|
357
|
+
|
|
358
|
+
@param port: 串口(如 '/dev/ttyACM0');None 时按 USB_VID=0x303A 自动探测,
|
|
359
|
+
检测到多个设备时会交互式询问选择
|
|
360
|
+
@param confirm: 烧录前交互确认(默认 True);脚本化场景可传 False 跳过
|
|
361
|
+
@return: (是否成功, esptool 输出或错误信息)
|
|
362
|
+
@note: 烧录前会释放当前占用的串口;烧录后设备复位,需重新创建 MXDBG() 实例
|
|
363
|
+
"""
|
|
364
|
+
# 释放已占用的串口(esptool 需要独占端口)
|
|
365
|
+
if getattr(self, "_client", None) is not None and self._client.is_open:
|
|
366
|
+
self._client.close()
|
|
367
|
+
|
|
368
|
+
fw_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "firmware")
|
|
369
|
+
firmware = {
|
|
370
|
+
name: os.path.join(fw_dir, name)
|
|
371
|
+
for name in ("bootloader.bin", "partition-table.bin", "mxdbg.bin")
|
|
372
|
+
}
|
|
373
|
+
for path in firmware.values():
|
|
374
|
+
if not os.path.exists(path):
|
|
375
|
+
return (
|
|
376
|
+
False,
|
|
377
|
+
(
|
|
378
|
+
f"firmware not bundled (missing {os.path.basename(path)}). "
|
|
379
|
+
"Reinstall mxdbg with firmware included."
|
|
380
|
+
),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
if port is None:
|
|
384
|
+
# 多设备时交互式选择
|
|
385
|
+
found = self.__select_port()
|
|
386
|
+
if found is None:
|
|
387
|
+
return (
|
|
388
|
+
False,
|
|
389
|
+
(
|
|
390
|
+
"No device found. Connect ESP32 and check serial "
|
|
391
|
+
"permissions (Linux: add user to dialout group)."
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
port = found
|
|
395
|
+
elif not re.fullmatch(r"[A-Za-z0-9_/.-]+", port):
|
|
396
|
+
return False, f"invalid port: {port!r}"
|
|
397
|
+
|
|
398
|
+
if confirm and not self.__confirm_flash(port):
|
|
399
|
+
return False, "flashing cancelled by user."
|
|
400
|
+
|
|
401
|
+
return self.__esptool_flash(port, firmware)
|
|
402
|
+
|
|
403
|
+
def __select_port(self) -> str | None:
|
|
404
|
+
"""探测 ESP32 设备;多个时交互式询问选择,返回完整端口路径或 None。"""
|
|
405
|
+
ports = self.__find_ports()
|
|
406
|
+
if not ports:
|
|
407
|
+
return None
|
|
408
|
+
if sys.platform != "win32":
|
|
409
|
+
ports = [f"/dev/{p}" for p in ports]
|
|
410
|
+
if len(ports) == 1:
|
|
411
|
+
return ports[0]
|
|
412
|
+
|
|
413
|
+
print("检测到多个 ESP32 设备:")
|
|
414
|
+
for i, p in enumerate(ports):
|
|
415
|
+
print(f" [{i}] {p}")
|
|
416
|
+
try:
|
|
417
|
+
choice = input(
|
|
418
|
+
f"请选择要烧录的设备 [0-{len(ports) - 1}] (默认 0): "
|
|
419
|
+
).strip()
|
|
420
|
+
except EOFError:
|
|
421
|
+
return ports[0]
|
|
422
|
+
if choice == "":
|
|
423
|
+
return ports[0]
|
|
424
|
+
try:
|
|
425
|
+
idx = int(choice)
|
|
426
|
+
except ValueError:
|
|
427
|
+
return None
|
|
428
|
+
return ports[idx] if 0 <= idx < len(ports) else None
|
|
429
|
+
|
|
430
|
+
def __confirm_flash(self, port: str) -> bool:
|
|
431
|
+
"""烧录前交互确认。"""
|
|
432
|
+
try:
|
|
433
|
+
answer = input(f"确认烧录固件到 {port}? [y/N] ").strip().lower()
|
|
434
|
+
except EOFError:
|
|
435
|
+
return False
|
|
436
|
+
return answer in ("y", "yes")
|
|
437
|
+
|
|
438
|
+
def __esptool_flash(self, port: str, firmware: dict[str, str]) -> tuple[bool, str]:
|
|
439
|
+
"""调用 esptool 烧录固件;烧录参数与构建配置一致(build/flasher_args.json):dio / 2MB / 80m。"""
|
|
440
|
+
cmd = [
|
|
441
|
+
sys.executable,
|
|
442
|
+
"-m",
|
|
443
|
+
"esptool",
|
|
444
|
+
"--chip",
|
|
445
|
+
"esp32s3",
|
|
446
|
+
"-p",
|
|
447
|
+
port,
|
|
448
|
+
"--before",
|
|
449
|
+
"default_reset",
|
|
450
|
+
"--after",
|
|
451
|
+
"hard_reset",
|
|
452
|
+
"write_flash",
|
|
453
|
+
"--flash_mode",
|
|
454
|
+
"dio",
|
|
455
|
+
"--flash_size",
|
|
456
|
+
"2MB",
|
|
457
|
+
"--flash_freq",
|
|
458
|
+
"80m",
|
|
459
|
+
"0x0",
|
|
460
|
+
firmware["bootloader.bin"],
|
|
461
|
+
"0x8000",
|
|
462
|
+
firmware["partition-table.bin"],
|
|
463
|
+
"0x10000",
|
|
464
|
+
firmware["mxdbg.bin"],
|
|
465
|
+
]
|
|
466
|
+
try:
|
|
467
|
+
result = subprocess.run(
|
|
468
|
+
cmd, capture_output=True, text=True, timeout=300, check=False
|
|
469
|
+
)
|
|
470
|
+
except FileNotFoundError:
|
|
471
|
+
return False, "esptool not installed. Run: pip install esptool"
|
|
472
|
+
except subprocess.TimeoutExpired:
|
|
473
|
+
return (
|
|
474
|
+
False,
|
|
475
|
+
(
|
|
476
|
+
"esptool timed out. If using USB-JTAG, hold BOOT + press RST "
|
|
477
|
+
"to enter download mode."
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
output = (result.stdout + result.stderr).strip()
|
|
482
|
+
if result.returncode != 0:
|
|
483
|
+
hint = ""
|
|
484
|
+
if "Failed to connect" in output:
|
|
485
|
+
hint = " (connect failed: hold BOOT + press RST to enter download mode)"
|
|
486
|
+
return False, output + hint
|
|
487
|
+
return True, output or "Flash OK"
|
|
488
|
+
|
|
489
|
+
def __read(self, timeout=2) -> bytearray:
|
|
490
|
+
|
|
491
|
+
data = bytearray()
|
|
492
|
+
start_time = time.time()
|
|
493
|
+
|
|
494
|
+
while True:
|
|
495
|
+
# 检查超时
|
|
496
|
+
if time.time() - start_time > timeout:
|
|
497
|
+
if len(data) == 0:
|
|
498
|
+
raise TimeoutError("Data read timeout. No data received.")
|
|
499
|
+
else:
|
|
500
|
+
raise TimeoutError(f"Data read timeout. Data received: {data}")
|
|
501
|
+
|
|
502
|
+
if self._client.in_waiting > 0:
|
|
503
|
+
temp_data = self._client.read(self._client.in_waiting)
|
|
504
|
+
data += temp_data
|
|
505
|
+
|
|
506
|
+
if (
|
|
507
|
+
len(data) >= 5
|
|
508
|
+
and self.__check_crc(data)
|
|
509
|
+
and data[:5] == bytearray("mxdbg", "utf-8")
|
|
510
|
+
):
|
|
511
|
+
break
|
|
512
|
+
|
|
513
|
+
time.sleep(0.01)
|
|
514
|
+
|
|
515
|
+
return data
|
|
516
|
+
|
|
517
|
+
def __write(self, data) -> None:
|
|
518
|
+
self._client.write(data)
|
|
519
|
+
|
|
520
|
+
def __task_execute(self, cmd, data: list) -> tuple:
|
|
521
|
+
|
|
522
|
+
write_data = self.__data_pack(cmd, data)
|
|
523
|
+
|
|
524
|
+
try:
|
|
525
|
+
self.__write(write_data)
|
|
526
|
+
except Exception as e:
|
|
527
|
+
raise ValueError(f"Failed to write data: {e}")
|
|
528
|
+
|
|
529
|
+
read_data = self.__read()
|
|
530
|
+
|
|
531
|
+
ret, temp_data = self.__data_unpack(cmd, read_data)
|
|
532
|
+
|
|
533
|
+
return ret, temp_data
|
|
534
|
+
|
|
535
|
+
def __data_decompose(self, data: int, bytes_num: int = 4) -> tuple:
|
|
536
|
+
return [(data >> (8 * (bytes_num - 1 - i))) & 0xFF for i in range(bytes_num)]
|
|
537
|
+
|
|
538
|
+
def __hexdump(self, data: bytearray, base_address=0x3FC9900C) -> None:
|
|
539
|
+
logger.debug("hex:")
|
|
540
|
+
|
|
541
|
+
# 按行分组数据,每行16字节
|
|
542
|
+
for i in range(0, len(data), 16):
|
|
543
|
+
# 显示16字节的 hex 值,每8字节后加一个 '\t'
|
|
544
|
+
line_data = data[i : i + 16]
|
|
545
|
+
for j, byte in enumerate(line_data):
|
|
546
|
+
print(f"{byte:02X} ", end="")
|
|
547
|
+
if (j + 1) % 8 == 0:
|
|
548
|
+
print("\t", end="")
|
|
549
|
+
|
|
550
|
+
# 计算并显示 ASCII 表示,非可见字符替换为 '.'
|
|
551
|
+
ascii_repr = "".join(chr(b) if 32 <= b <= 126 else "." for b in line_data)
|
|
552
|
+
print(f"|{ascii_repr}|")
|
|
553
|
+
|
|
554
|
+
def __calculate_crc(self, data: bytearray) -> bytearray:
|
|
555
|
+
"""
|
|
556
|
+
@brief Calculate CRC16 for the given data.
|
|
557
|
+
@param data: Data to calculate CRC16.
|
|
558
|
+
@return: CRC16 value.
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
crc = 0xFFFF
|
|
562
|
+
|
|
563
|
+
for byte in data:
|
|
564
|
+
crc ^= byte
|
|
565
|
+
for _ in range(8):
|
|
566
|
+
crc = (crc >> 1) ^ (0xA001 if (crc & 1) else 0)
|
|
567
|
+
|
|
568
|
+
high, low = ((crc >> 8) & 0xFF), (crc & 0xFF)
|
|
569
|
+
|
|
570
|
+
return bytearray([high, low])
|
|
571
|
+
|
|
572
|
+
def __check_crc(self, data: bytearray) -> bool:
|
|
573
|
+
"""
|
|
574
|
+
@brief Check CRC16 for the given data.
|
|
575
|
+
@param data: Data to check CRC16.
|
|
576
|
+
@return: True if CRC16 is correct, False otherwise.
|
|
577
|
+
"""
|
|
578
|
+
|
|
579
|
+
if not self.__crc_enable:
|
|
580
|
+
return True
|
|
581
|
+
|
|
582
|
+
high, low = self.__calculate_crc(data[:-2])
|
|
583
|
+
|
|
584
|
+
# logger.debug(f"CRC Check: {high}, {low}, {data[-2]}, {data[-1]}")
|
|
585
|
+
|
|
586
|
+
return bool(high == data[-2] and low == data[-1])
|
|
587
|
+
|
|
588
|
+
def __data_pack(self, cmd: int, data: list) -> bytearray:
|
|
589
|
+
"""
|
|
590
|
+
@brief Package data with header and CRC16.
|
|
591
|
+
@param data: Data to package.
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
# data sent format
|
|
595
|
+
# | 5 bytes | 1 byte | 1 byte | 1 byte | n bytes | 1 byte | 2 bytes |
|
|
596
|
+
# |---------|--------|--------|--------|---------|--------|---------|
|
|
597
|
+
# | mxdbg | : | cmd | : | data | : | crc |
|
|
598
|
+
|
|
599
|
+
temp_data = bytearray("mxdbg:", "utf-8")
|
|
600
|
+
temp_data += bytearray([cmd])
|
|
601
|
+
temp_data += bytearray(":", "utf-8")
|
|
602
|
+
temp_data += bytearray(data)
|
|
603
|
+
temp_data += bytearray(":", "utf-8")
|
|
604
|
+
|
|
605
|
+
high, low = self.__calculate_crc(temp_data)
|
|
606
|
+
temp_data += bytearray([high, low])
|
|
607
|
+
|
|
608
|
+
return temp_data
|
|
609
|
+
|
|
610
|
+
def __data_unpack(self, cmd, data: bytearray) -> tuple:
|
|
611
|
+
"""
|
|
612
|
+
@brief Unpackage data.
|
|
613
|
+
@param data: Data to unpackage.
|
|
614
|
+
@return: cmd and data.
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
# data received format
|
|
618
|
+
# | 5 bytes | 1 byte | 1 byte | 1 byte | 4 bytes | 1 byte | n bytes | 1 byte | 2 bytes |
|
|
619
|
+
# |---------|--------|--------|--------|---------|--------|---------|--------|---------|
|
|
620
|
+
# | mxdbg | : | cmd | : | ret | : | data | : | crc |
|
|
621
|
+
|
|
622
|
+
# logger.debug("Data: {}".format(["{:02X}".format(d) for d in list(data)]))
|
|
623
|
+
|
|
624
|
+
# check crc first
|
|
625
|
+
temp_data = data
|
|
626
|
+
if not self.__check_crc(temp_data):
|
|
627
|
+
raise ValueError("CRC check failed.")
|
|
628
|
+
|
|
629
|
+
# check header
|
|
630
|
+
header = data[:5]
|
|
631
|
+
if header != bytearray("mxdbg", "utf-8"):
|
|
632
|
+
raise ValueError("Invalid header.")
|
|
633
|
+
|
|
634
|
+
# check cmd
|
|
635
|
+
cmd_ = data[6]
|
|
636
|
+
if cmd_ != cmd:
|
|
637
|
+
raise ValueError("Invalid cmd.")
|
|
638
|
+
|
|
639
|
+
ret = (
|
|
640
|
+
data[8:12][0] << 24
|
|
641
|
+
| data[8:12][1] << 16
|
|
642
|
+
| data[8:12][2] << 8
|
|
643
|
+
| data[8:12][3]
|
|
644
|
+
)
|
|
645
|
+
ret = ctypes.c_int32(ret).value
|
|
646
|
+
|
|
647
|
+
if len(data) > 15: # data exists
|
|
648
|
+
temp_data = data[13:-3]
|
|
649
|
+
else:
|
|
650
|
+
temp_data = None
|
|
651
|
+
|
|
652
|
+
return ret, temp_data
|
|
653
|
+
|
|
654
|
+
def __parse_version(self, lines) -> tuple[int, int]:
|
|
655
|
+
"""Parse VERSION_MAJOR and VERSION_MINOR from header file."""
|
|
656
|
+
try:
|
|
657
|
+
major_pattern = re.compile(r"#define\s+VERSION_MAJOR\s+(\d+)")
|
|
658
|
+
minor_pattern = re.compile(r"#define\s+VERSION_MINOR\s+(\d+)")
|
|
659
|
+
|
|
660
|
+
for line in lines:
|
|
661
|
+
major_match = major_pattern.search(line)
|
|
662
|
+
if major_match:
|
|
663
|
+
version_major = int(major_match.group(1))
|
|
664
|
+
|
|
665
|
+
minor_match = minor_pattern.search(line)
|
|
666
|
+
if minor_match:
|
|
667
|
+
version_minor = int(minor_match.group(1))
|
|
668
|
+
except Exception as e:
|
|
669
|
+
raise ValueError(f"Error parsing version: {e}")
|
|
670
|
+
|
|
671
|
+
return version_major, version_minor
|
|
672
|
+
|
|
673
|
+
def __parse_task_cmd(self, lines) -> dict:
|
|
674
|
+
"""Parse TASK commands from header file."""
|
|
675
|
+
task_cmd = {}
|
|
676
|
+
try:
|
|
677
|
+
cmd_pattern = re.compile(r"TASK_(\w+)\s*=\s*(0x[0-9A-Fa-f]+|\d+),?")
|
|
678
|
+
for line in lines:
|
|
679
|
+
match = cmd_pattern.search(line)
|
|
680
|
+
if match:
|
|
681
|
+
cmd_name = f"TASK_{match.group(1)}"
|
|
682
|
+
cmd_id = int(match.group(2), 0)
|
|
683
|
+
task_cmd[cmd_name] = cmd_id
|
|
684
|
+
except Exception as e:
|
|
685
|
+
raise ValueError(f"Error parsing task commands: {e}")
|
|
686
|
+
return task_cmd
|
|
687
|
+
|
|
688
|
+
def __parse_error_map(self, lines, task_cmd, version_major, version_minor) -> dict:
|
|
689
|
+
"""Parse error codes from header file, including MXDBG_ERR and ESPRESSIF_ERR."""
|
|
690
|
+
error_map = {}
|
|
691
|
+
try:
|
|
692
|
+
# Regular expression to match ERR_SYNTHESIS with parameters
|
|
693
|
+
err_synthesis_pattern = re.compile(
|
|
694
|
+
r"#define\s+(MXDBG_ERR_\w+)\s+ERR_SYNTHESIS\((.+?)\)\s*/\*!\s*(.+?)\s*\*/"
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# Regular expression to match simpler error patterns (e.g., ESPRESSIF_ERR)
|
|
698
|
+
simple_error_pattern = re.compile(
|
|
699
|
+
r"#define\s+(ESPRESSIF_ERR_\w+|ESPRESSIF_[A-Z_]+)\s+(0x[0-9A-Fa-f]+|\d+)\s*/\*!\s*(.+?)\s*\*/"
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
# Parse ERR_SYNTHESIS-based errors
|
|
703
|
+
for line in lines:
|
|
704
|
+
match = err_synthesis_pattern.search(line)
|
|
705
|
+
if match:
|
|
706
|
+
error_name = match.group(1) # e.g., "MXDBG_ERR_SPI_CONFIG_FAILED"
|
|
707
|
+
synthesis_args = match.group(2) # e.g., "TASK_SPI_CONFIG, 0x00"
|
|
708
|
+
error_desc = match.group(3).strip()
|
|
709
|
+
|
|
710
|
+
# Parse ERR_SYNTHESIS arguments
|
|
711
|
+
args = synthesis_args.split(",")
|
|
712
|
+
if len(args) == 2:
|
|
713
|
+
task_name = args[0].strip()
|
|
714
|
+
ret_code = int(args[1].strip(), 0)
|
|
715
|
+
|
|
716
|
+
# Use numeric literal directly, otherwise look up in task_cmd
|
|
717
|
+
task_id = (
|
|
718
|
+
int(task_name, 0)
|
|
719
|
+
if task_name.startswith("0x")
|
|
720
|
+
else task_cmd.get(task_name, 0xFF)
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# Compute error code based on ERR_SYNTHESIS logic
|
|
724
|
+
version_combined = (version_major << 24) | (version_minor << 16)
|
|
725
|
+
error_code = version_combined | (task_id << 8) | ret_code
|
|
726
|
+
|
|
727
|
+
error_map[error_name] = {
|
|
728
|
+
"code": ctypes.c_int32(error_code).value,
|
|
729
|
+
"desc": error_desc.replace("< ", ""),
|
|
730
|
+
}
|
|
731
|
+
continue
|
|
732
|
+
|
|
733
|
+
# Parse simpler error patterns (ESPRESSIF_ERR)
|
|
734
|
+
match = simple_error_pattern.search(line)
|
|
735
|
+
if match:
|
|
736
|
+
error_name = match.group(1)
|
|
737
|
+
error_code = int(match.group(2), 0)
|
|
738
|
+
error_desc = match.group(3).strip()
|
|
739
|
+
# error_map[error_name] = (ctypes.c_int32(error_code).value, error_desc.replace('< ', ''))
|
|
740
|
+
error_map[error_name] = {
|
|
741
|
+
"code": ctypes.c_int32(error_code).value,
|
|
742
|
+
"desc": error_desc.replace("< ", ""),
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
except Exception as e:
|
|
746
|
+
raise ValueError(f"Error parsing error map: {e}")
|
|
747
|
+
|
|
748
|
+
return error_map
|
|
749
|
+
|
|
750
|
+
def __compute_error_code(
|
|
751
|
+
self, version_major, version_minor, task_id, ret_code
|
|
752
|
+
) -> int:
|
|
753
|
+
"""Compute the error code based on task_id and ret_code."""
|
|
754
|
+
return (version_major << 24) | (version_minor << 16) | (task_id << 8) | ret_code
|
|
755
|
+
|
|
756
|
+
def __save_to_toml(self, path, version, task_cmd, error_map) -> None:
|
|
757
|
+
"""Save parsed data to TOML file with additional annotations for version."""
|
|
758
|
+
try:
|
|
759
|
+
data = {"version": version, "task_cmd": task_cmd, "error_map": error_map}
|
|
760
|
+
|
|
761
|
+
with open(path, "w") as f:
|
|
762
|
+
toml.dump(data, f)
|
|
763
|
+
|
|
764
|
+
except Exception as e:
|
|
765
|
+
raise ValueError(f"Failed to save mxdbg.toml: {e}")
|
|
766
|
+
|
|
767
|
+
def __parse_and_map(self) -> tuple:
|
|
768
|
+
|
|
769
|
+
version = 0x00
|
|
770
|
+
task_cmd = {}
|
|
771
|
+
error_map = {}
|
|
772
|
+
|
|
773
|
+
header_file = os.path.join(os.path.dirname(__file__), "../main/mxdbg.h")
|
|
774
|
+
if os.path.exists(header_file):
|
|
775
|
+
self.__mxdbg_header_file = header_file
|
|
776
|
+
self.__mxdbg_toml_path = os.path.join(
|
|
777
|
+
os.path.dirname(__file__), "mxdbg.toml"
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
else:
|
|
781
|
+
for f in os.listdir(os.path.dirname(__file__)):
|
|
782
|
+
if f.endswith(".toml"):
|
|
783
|
+
if f == "mxdbg.toml":
|
|
784
|
+
self.__mxdbg_toml_path = os.path.join(
|
|
785
|
+
os.path.dirname(__file__), f
|
|
786
|
+
)
|
|
787
|
+
break
|
|
788
|
+
elif f.endswith(".h") and f == "mxdbg.h":
|
|
789
|
+
self.__mxdbg_header_file = os.path.join(
|
|
790
|
+
os.path.dirname(__file__), f
|
|
791
|
+
)
|
|
792
|
+
break
|
|
793
|
+
|
|
794
|
+
if self.__mxdbg_toml_path is None and self.__mxdbg_header_file is None:
|
|
795
|
+
raise FileNotFoundError(
|
|
796
|
+
"Please provide mxdbg.toml or mxdbg.h file to parse."
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
if self.__mxdbg_header_file:
|
|
800
|
+
try:
|
|
801
|
+
with open(self.__mxdbg_header_file, "r") as f:
|
|
802
|
+
lines = f.readlines()
|
|
803
|
+
|
|
804
|
+
version_major, version_minor = self.__parse_version(lines)
|
|
805
|
+
version = {"MAJOR": version_major, "MINOR": version_minor}
|
|
806
|
+
|
|
807
|
+
task_cmd = self.__parse_task_cmd(lines)
|
|
808
|
+
|
|
809
|
+
error_map = self.__parse_error_map(
|
|
810
|
+
lines, task_cmd, version_major, version_minor
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
self.__save_to_toml(
|
|
814
|
+
self.__mxdbg_toml_path, version, task_cmd, error_map
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
except Exception as e:
|
|
818
|
+
raise ValueError(f"Failed to parse error map: {e}")
|
|
819
|
+
|
|
820
|
+
else:
|
|
821
|
+
try:
|
|
822
|
+
with open(self.__mxdbg_toml_path, "r") as f:
|
|
823
|
+
data = toml.load(f)
|
|
824
|
+
|
|
825
|
+
version = data.get("version", version)
|
|
826
|
+
task_cmd = data.get("task_cmd", task_cmd)
|
|
827
|
+
error_map = data.get("error_map", error_map)
|
|
828
|
+
|
|
829
|
+
except Exception as e:
|
|
830
|
+
raise ValueError(f"Failed to load mxdbg.toml: {e}")
|
|
831
|
+
|
|
832
|
+
return version, task_cmd, error_map
|
|
833
|
+
|
|
834
|
+
def __check_ret_code(self, cmd: int, ret: int) -> None:
|
|
835
|
+
|
|
836
|
+
if ret != 0:
|
|
837
|
+
error_code = self.__compute_error_code(
|
|
838
|
+
self.version["MAJOR"], self.version["MINOR"], cmd, ret
|
|
839
|
+
)
|
|
840
|
+
error_desc = "Unknown error."
|
|
841
|
+
for key in self.__error_map:
|
|
842
|
+
if self.__error_map[key]["code"] == error_code:
|
|
843
|
+
error_desc = self.__error_map[key]["desc"]
|
|
844
|
+
break
|
|
845
|
+
logger.error(
|
|
846
|
+
f"Error code: 0x{ctypes.c_uint32(error_code).value:08X}, Description: {error_desc}"
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
def i2c_find_slave(self, port: int = 0) -> tuple[bool, list | None]:
|
|
850
|
+
"""
|
|
851
|
+
@brief Find I2C slave devices.
|
|
852
|
+
@param port: I2C port number. Default is `0`.
|
|
853
|
+
@return: List of I2C slave devices.
|
|
854
|
+
"""
|
|
855
|
+
|
|
856
|
+
found_device_list = []
|
|
857
|
+
for slave_id in range(0x04, 0x7F):
|
|
858
|
+
try:
|
|
859
|
+
ret, _data = self.i2c_write_read(slave_id, [0x00], 0, port=port)
|
|
860
|
+
if ret:
|
|
861
|
+
found_device_list.append(slave_id)
|
|
862
|
+
except Exception:
|
|
863
|
+
continue
|
|
864
|
+
|
|
865
|
+
if found_device_list != []:
|
|
866
|
+
found_device_list = [f"0x{slave_id:02X}" for slave_id in found_device_list]
|
|
867
|
+
return True, found_device_list
|
|
868
|
+
else:
|
|
869
|
+
return False, None
|
|
870
|
+
|
|
871
|
+
def i2c_write_read(
|
|
872
|
+
self,
|
|
873
|
+
slave_id: int,
|
|
874
|
+
write_list: list,
|
|
875
|
+
read_length: int,
|
|
876
|
+
port: int = 0,
|
|
877
|
+
slave_id_10_bit: bool = False,
|
|
878
|
+
) -> tuple[bool, list | None]:
|
|
879
|
+
"""
|
|
880
|
+
@brief Write and read data from I2C slave device. The default I2C pin is SDA: `10`, SCL: `11`.
|
|
881
|
+
@param slave_id: I2C slave device address. (e.g., `0x04`.)
|
|
882
|
+
@param write_list: Data to write to I2C slave device. (It should be a list of bytes. e.g., `[0x00, 0x01]` or `[]`.)
|
|
883
|
+
@param read_length: Length of data to read from I2C slave device.
|
|
884
|
+
@param port: I2C port number.( It should be `0` (in default) or `1`.)
|
|
885
|
+
@param slave_id_10_bit: True if 10-bit slave address, False otherwise.
|
|
886
|
+
@return: Data read from I2C slave device.
|
|
887
|
+
"""
|
|
888
|
+
|
|
889
|
+
if port not in [0, 1]:
|
|
890
|
+
logger.error("Invalid port number, the available port number is 0 or 1.")
|
|
891
|
+
raise ValueError(
|
|
892
|
+
"Invalid port number, the available port number is 0 or 1."
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
if slave_id < 0x04 or slave_id > 0x07FF:
|
|
896
|
+
logger.error(
|
|
897
|
+
"Invalid slave, the available slave id is between 0x04 and 0x07FF."
|
|
898
|
+
)
|
|
899
|
+
raise ValueError(
|
|
900
|
+
"Invalid slave, the available slave id is between 0x04 and 0x07FF."
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
# 构造要发送的数据包
|
|
904
|
+
slave_id = slave_id if not slave_id_10_bit else slave_id & 0x07FF
|
|
905
|
+
write_len = len(write_list)
|
|
906
|
+
read_len = read_length
|
|
907
|
+
|
|
908
|
+
if write_len <= 0 and read_len <= 0:
|
|
909
|
+
logger.error("Write length and read length should be greater than 0.")
|
|
910
|
+
raise ValueError("Write length and read length should be greater than 0.")
|
|
911
|
+
|
|
912
|
+
"""
|
|
913
|
+
* value format received:
|
|
914
|
+
* | 5 bytes | 1 byte | 1 byte | 1 byte | 1 byte | 2 bytes | 4 bytes | 4 bytes | n bytes | 1 byte | 2 bytes |
|
|
915
|
+
* |---------|--------|--------|--------|----------|----------|--------------|-------------|-----------------|--------|---------|
|
|
916
|
+
* | mxdbg | : | cmd | : | i2c_port | slave_id | write_length | read_length | write_data_list | : | crc |
|
|
917
|
+
"""
|
|
918
|
+
|
|
919
|
+
i2c_data_temp = [
|
|
920
|
+
port,
|
|
921
|
+
(slave_id & 0xFF00) >> 8,
|
|
922
|
+
slave_id & 0x00FF,
|
|
923
|
+
(write_len & 0xFF000000) >> 24,
|
|
924
|
+
(write_len & 0x00FF0000) >> 16,
|
|
925
|
+
(write_len & 0x0000FF00) >> 8,
|
|
926
|
+
write_len & 0x000000FF,
|
|
927
|
+
(read_len & 0xFF000000) >> 24,
|
|
928
|
+
(read_len & 0x00FF0000) >> 16,
|
|
929
|
+
(read_len & 0x0000FF00) >> 8,
|
|
930
|
+
read_len & 0x000000FF,
|
|
931
|
+
]
|
|
932
|
+
i2c_data_temp += write_list
|
|
933
|
+
|
|
934
|
+
# 执行任务并读取返回的数据
|
|
935
|
+
ret, data = self.__task_execute(
|
|
936
|
+
self.task_cmd["TASK_I2C_WRITE_READ"], i2c_data_temp
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
data = list(data) if data is not None else None
|
|
940
|
+
|
|
941
|
+
if ret != 0:
|
|
942
|
+
# logger.error(f"Error: {ret}")
|
|
943
|
+
return False, None
|
|
944
|
+
else:
|
|
945
|
+
return True, data
|
|
946
|
+
|
|
947
|
+
def i2c_config(
|
|
948
|
+
self,
|
|
949
|
+
port: int = 0,
|
|
950
|
+
freq: int = 400000,
|
|
951
|
+
sda_pin: int = 10,
|
|
952
|
+
scl_pin: int = 11,
|
|
953
|
+
sda_pullup: bool = True,
|
|
954
|
+
scl_pullup: bool = True,
|
|
955
|
+
) -> bool | tuple[bool, None]:
|
|
956
|
+
"""
|
|
957
|
+
@brief Configure I2C port. ***Please do not modify the configuration of PORT1***.
|
|
958
|
+
@param port: I2C port number.( It should be `0` (in default) or `1`.)
|
|
959
|
+
@param freq: I2C frequency. (Default is `400000`.)
|
|
960
|
+
@param sda_pin: SDA pin number. (Default is `10`.)
|
|
961
|
+
@param scl_pin: SCL pin number. (Default is `11`.)
|
|
962
|
+
@param sda_pullup: SDA pull up resistor. (`True`: enable, `False`: disable.)
|
|
963
|
+
@param scl_pullup: SCL pull up resistor. (`True`: enable, `False`: disable.)
|
|
964
|
+
@return: Return True if success, False otherwise.
|
|
965
|
+
"""
|
|
966
|
+
|
|
967
|
+
if port not in [0, 1]:
|
|
968
|
+
logger.error("Invalid port number.")
|
|
969
|
+
return False
|
|
970
|
+
|
|
971
|
+
self.__i2c_valid_pins = None
|
|
972
|
+
|
|
973
|
+
if (sda_pin not in self.i2c_valid_pins) or (scl_pin not in self.i2c_valid_pins):
|
|
974
|
+
logger.error("Invalid pin number.")
|
|
975
|
+
return False, None
|
|
976
|
+
elif sda_pin in self.__i2c_used_pins and scl_pin in self.__i2c_used_pins:
|
|
977
|
+
pass
|
|
978
|
+
|
|
979
|
+
if sda_pin == scl_pin:
|
|
980
|
+
logger.error("SDA and SCL pins should be different.")
|
|
981
|
+
return False, None
|
|
982
|
+
|
|
983
|
+
port = ctypes.c_uint8(port).value
|
|
984
|
+
freq = ctypes.c_uint32(freq).value
|
|
985
|
+
sda_pin = ctypes.c_uint8(sda_pin).value
|
|
986
|
+
scl_pin = ctypes.c_uint8(scl_pin).value
|
|
987
|
+
sda_pullup = 0xF if sda_pullup else 0x0
|
|
988
|
+
scl_pullup = 0xF if scl_pullup else 0x0
|
|
989
|
+
|
|
990
|
+
i2c_data_temp = [
|
|
991
|
+
port,
|
|
992
|
+
(freq & 0xFF000000) >> 24,
|
|
993
|
+
(freq & 0x00FF0000) >> 16,
|
|
994
|
+
(freq & 0x0000FF00) >> 8,
|
|
995
|
+
freq & 0x000000FF,
|
|
996
|
+
sda_pin,
|
|
997
|
+
scl_pin,
|
|
998
|
+
(sda_pullup << 4 | scl_pullup),
|
|
999
|
+
]
|
|
1000
|
+
|
|
1001
|
+
ret, _data = self.__task_execute(
|
|
1002
|
+
self.task_cmd["TASK_I2C_CONFIG"], i2c_data_temp
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
if ret != 0:
|
|
1006
|
+
logger.error(f"Error: {ret}")
|
|
1007
|
+
return False, None
|
|
1008
|
+
else:
|
|
1009
|
+
for pin in self.__i2c_used_pins:
|
|
1010
|
+
self.mark_pin_free(pin)
|
|
1011
|
+
for pin in [sda_pin, scl_pin]:
|
|
1012
|
+
self.__mark_i2c_used(pin)
|
|
1013
|
+
return True, None
|
|
1014
|
+
|
|
1015
|
+
def gpio_write_read(
|
|
1016
|
+
self, pin: int, level: int | None = None
|
|
1017
|
+
) -> tuple[bool, int | None]:
|
|
1018
|
+
"""
|
|
1019
|
+
@brief Write or read GPIO pin. This API will read GPIO's level if you set `level` to `None`, otherwise it will be in write mode.
|
|
1020
|
+
@param pin: GPIO pin number.
|
|
1021
|
+
@param level: GPIO level. (`0`: Low, `1`: High. `None` in default.)
|
|
1022
|
+
@return: Return True and data if success, False otherwise.
|
|
1023
|
+
"""
|
|
1024
|
+
|
|
1025
|
+
if pin not in self.gpio_valid_pins:
|
|
1026
|
+
logger.error("Invalid pin number.")
|
|
1027
|
+
return False, None
|
|
1028
|
+
|
|
1029
|
+
pin = ctypes.c_uint8(pin).value
|
|
1030
|
+
|
|
1031
|
+
if level is None: # read mode
|
|
1032
|
+
operation = 1
|
|
1033
|
+
gpio_data_temp = [operation, pin, 0]
|
|
1034
|
+
else: # write mode
|
|
1035
|
+
operation = 0
|
|
1036
|
+
gpio_data_temp = [operation, pin, ((ctypes.c_uint8(level).value) & 0x01)]
|
|
1037
|
+
|
|
1038
|
+
ret, data = self.__task_execute(
|
|
1039
|
+
self.task_cmd["TASK_GPIO_WRITE_READ"], gpio_data_temp
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
if ret == 0:
|
|
1043
|
+
return True, None if (operation == 0) else data[0]
|
|
1044
|
+
else:
|
|
1045
|
+
logger.error(f"Error: {ret}")
|
|
1046
|
+
return False, None
|
|
1047
|
+
|
|
1048
|
+
def gpio_config(self, pin: int, mode: int, pull_up: bool, pull_down: bool) -> bool:
|
|
1049
|
+
"""
|
|
1050
|
+
@brief Configure GPIO pin.
|
|
1051
|
+
@param pin: GPIO pin number.
|
|
1052
|
+
@param mode: GPIO mode. (`0x00`: Disable, `0x01`: Input, `0x02`: Output, `0x03`: Input/Output, `0x06`: Output Open Drain, `0x07`: Input/Output Open Drain)
|
|
1053
|
+
@param pull_up: Pull up resistor. (`True`: Enable, `False`: Disable.)
|
|
1054
|
+
@param pull_down: Pull down resistor. (`True`: Enable, `False`: Disable.)
|
|
1055
|
+
@return: Return value. 0: Success, other: Error.
|
|
1056
|
+
"""
|
|
1057
|
+
|
|
1058
|
+
self.__gpio_valid_pins = None
|
|
1059
|
+
|
|
1060
|
+
if pin not in self.gpio_valid_pins:
|
|
1061
|
+
logger.error("Invalid pin number.")
|
|
1062
|
+
raise ValueError("Invalid pin number.")
|
|
1063
|
+
|
|
1064
|
+
# assert mode in self.gpio_mode, "Invalid mode."
|
|
1065
|
+
if mode not in [v for k, v in self.gpio_mode.items()]:
|
|
1066
|
+
logger.error("Invalid mode.")
|
|
1067
|
+
raise ValueError("Invalid mode.")
|
|
1068
|
+
|
|
1069
|
+
pin = ctypes.c_uint8(pin).value
|
|
1070
|
+
mode = ctypes.c_uint8(mode).value
|
|
1071
|
+
pull_up = ctypes.c_uint8(0x01 if pull_up else 0x00).value
|
|
1072
|
+
pull_down = ctypes.c_uint8(0x01 if pull_down else 0x00).value
|
|
1073
|
+
|
|
1074
|
+
gpio_data_temp = [pin, mode, pull_up, pull_down]
|
|
1075
|
+
ret, _data = self.__task_execute(
|
|
1076
|
+
self.task_cmd["TASK_GPIO_CONFIG"], gpio_data_temp
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
if ret != 0:
|
|
1080
|
+
logger.error(f"Error: {ret}")
|
|
1081
|
+
return False
|
|
1082
|
+
else:
|
|
1083
|
+
self.__mark_gpio_used(pin)
|
|
1084
|
+
return True
|
|
1085
|
+
|
|
1086
|
+
def spi_write_read(
|
|
1087
|
+
self, write_list: list, read_length: int
|
|
1088
|
+
) -> tuple[bool, list | None]:
|
|
1089
|
+
"""
|
|
1090
|
+
@brief Write and read data from SPI slave device. (The default SPI pins are MISO: `12`, MOSI: `13`, SCLK: `14`, CS: `15`.)
|
|
1091
|
+
@param write_list: Data to write to SPI slave device. (It should be a list of bytes. e.g., `[0x00, 0x01]` or `[]`.)
|
|
1092
|
+
@param read_length: Length of data to read from SPI slave device.
|
|
1093
|
+
@return: Data read from SPI slave device.
|
|
1094
|
+
"""
|
|
1095
|
+
|
|
1096
|
+
write_len = ctypes.c_uint32(len(write_list)).value
|
|
1097
|
+
read_len = ctypes.c_uint32(read_length).value
|
|
1098
|
+
|
|
1099
|
+
if (write_len == 0) and (read_len == 0):
|
|
1100
|
+
logger.error("Write length and read length should not be zero.")
|
|
1101
|
+
return False, None
|
|
1102
|
+
else:
|
|
1103
|
+
if read_len > write_len:
|
|
1104
|
+
logger.error("Read length should not be larger than write length.")
|
|
1105
|
+
return False, None
|
|
1106
|
+
|
|
1107
|
+
spi_data_temp = [
|
|
1108
|
+
(write_len & 0xFF000000) >> 24,
|
|
1109
|
+
(write_len & 0x00FF0000) >> 16,
|
|
1110
|
+
(write_len & 0x0000FF00) >> 8,
|
|
1111
|
+
(write_len & 0x000000FF) >> 0,
|
|
1112
|
+
(read_len & 0xFF000000) >> 24,
|
|
1113
|
+
(read_len & 0x00FF0000) >> 16,
|
|
1114
|
+
(read_len & 0x0000FF00) >> 8,
|
|
1115
|
+
(read_len & 0x000000FF) >> 0,
|
|
1116
|
+
]
|
|
1117
|
+
spi_data_temp += write_list
|
|
1118
|
+
|
|
1119
|
+
ret, data = self.__task_execute(
|
|
1120
|
+
self.task_cmd["TASK_SPI_WRITE_READ"], spi_data_temp
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
self.__check_ret_code(self.task_cmd["TASK_SPI_WRITE_READ"], ret)
|
|
1124
|
+
|
|
1125
|
+
if ret == 0:
|
|
1126
|
+
if read_len == 0:
|
|
1127
|
+
return True, None
|
|
1128
|
+
else:
|
|
1129
|
+
return True, list(data)
|
|
1130
|
+
else:
|
|
1131
|
+
return False, None
|
|
1132
|
+
|
|
1133
|
+
# TODO: refactor 25+ parameters into a dataclass (小接口 principle)
|
|
1134
|
+
def spi_config(
|
|
1135
|
+
self,
|
|
1136
|
+
miso_io_num: int = 12,
|
|
1137
|
+
mosi_io_num: int = 13,
|
|
1138
|
+
sclk_io_num: int = 14,
|
|
1139
|
+
cs_io_num: int = 15,
|
|
1140
|
+
common_bus_flags: int = 0,
|
|
1141
|
+
freq: int = 1000000,
|
|
1142
|
+
mode: int = 3,
|
|
1143
|
+
quadwp_io_num: int = -1,
|
|
1144
|
+
quadhd_io_num: int = -1,
|
|
1145
|
+
data4_io_num: int = -1,
|
|
1146
|
+
data5_io_num: int = -1,
|
|
1147
|
+
data6_io_num: int = -1,
|
|
1148
|
+
data7_io_num: int = -1,
|
|
1149
|
+
max_transfer_sz: int = 4096,
|
|
1150
|
+
isr_cpu_id: int = 0,
|
|
1151
|
+
intr_flags: int = 0,
|
|
1152
|
+
command_bits: int = 0,
|
|
1153
|
+
address_bits: int = 0,
|
|
1154
|
+
dummy_bits: int = 0,
|
|
1155
|
+
duty_cycle_pos: int = 0,
|
|
1156
|
+
cs_ena_pretrans: int = 0,
|
|
1157
|
+
cs_ena_posttrans: int = 0,
|
|
1158
|
+
input_delay_ns: int = 0,
|
|
1159
|
+
device_interface_flags: int = 0,
|
|
1160
|
+
queue_size: int = 7,
|
|
1161
|
+
) -> tuple[bool, None]:
|
|
1162
|
+
"""
|
|
1163
|
+
@brief Configure SPI.
|
|
1164
|
+
@param miso_io_num: MISO pin number. (Default is `12`. Set to `-1` if not used.)
|
|
1165
|
+
@param mosi_io_num: MOSI pin number. (Default is `13`.)
|
|
1166
|
+
@param sclk_io_num: SCLK pin number.( Default is `14`.)
|
|
1167
|
+
@param cs_io_num: CS pin number. (Default is 15. Set to `-1` if not used.)
|
|
1168
|
+
@param quadwp_io_num: QUADWP pin number. (Default is `-1`.)
|
|
1169
|
+
@param quadhd_io_num: QUADHD pin number. (Default is `-1`.)
|
|
1170
|
+
@param data4_io_num: DATA4 pin number. (Default is `-1`.)
|
|
1171
|
+
@param data5_io_num: DATA5 pin number. (Default is `-1`.)
|
|
1172
|
+
@param data6_io_num: DATA6 pin number. (Default is `-1`.)
|
|
1173
|
+
@param data7_io_num: DATA7 pin number. (Default is `-1`.)
|
|
1174
|
+
@param max_transfer_sz: Maximum transfer size. (Default is `4096`.)
|
|
1175
|
+
@param common_bus_flags: Common bus flags. (Default is `0`.)
|
|
1176
|
+
@param isr_cpu_id: ISR CPU ID. (Default is `0`.)
|
|
1177
|
+
@param intr_flags: Interrupt flags. (Default is `0`.)
|
|
1178
|
+
@param command_bits: Command bits. (Default is `0`.)
|
|
1179
|
+
@param address_bits: Address bits. (Default is `0`.)
|
|
1180
|
+
@param dummy_bits: Dummy bits. (Default is `0`.)
|
|
1181
|
+
@param mode: SPI mode. (Default is `3`. Available values are `0`, `1`, `2`, `3`.)
|
|
1182
|
+
@param duty_cycle_pos: Duty cycle position. (Default is `0`.)
|
|
1183
|
+
@param cs_ena_pretrans: CS enable pretrans. (Default is `0`.)
|
|
1184
|
+
@param cs_ena_posttrans: CS enable posttrans. (Default is `0`.)
|
|
1185
|
+
@param freq: SPI frequency. (Default is `1000000`.)
|
|
1186
|
+
@param input_delay_ns: Input delay in ns. (Default is `0`.)
|
|
1187
|
+
@param device_interface_flags: Device interface flags. (Default is `0`. Set `(SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE)` if 3-wired half duplex mode.
|
|
1188
|
+
@param queue_size: Queue size. Default is `7`.)
|
|
1189
|
+
@return: Return True if success, False otherwise.
|
|
1190
|
+
"""
|
|
1191
|
+
|
|
1192
|
+
for pin in [
|
|
1193
|
+
mosi_io_num,
|
|
1194
|
+
miso_io_num,
|
|
1195
|
+
sclk_io_num,
|
|
1196
|
+
cs_io_num,
|
|
1197
|
+
quadhd_io_num,
|
|
1198
|
+
quadwp_io_num,
|
|
1199
|
+
data4_io_num,
|
|
1200
|
+
data5_io_num,
|
|
1201
|
+
data6_io_num,
|
|
1202
|
+
data7_io_num,
|
|
1203
|
+
]:
|
|
1204
|
+
if pin != -1 and (
|
|
1205
|
+
(pin not in self.spi_valid_pins) and (pin not in self.__spi_used_pins)
|
|
1206
|
+
):
|
|
1207
|
+
logger.error(f"Invalid pin number: {pin}")
|
|
1208
|
+
return False, None
|
|
1209
|
+
|
|
1210
|
+
if mode not in [0, 1, 2, 3]:
|
|
1211
|
+
raise ValueError("Invalid mode.")
|
|
1212
|
+
if common_bus_flags not in self.spi_common_bus_flag.values():
|
|
1213
|
+
raise ValueError("Invalid common bus flags.")
|
|
1214
|
+
if isr_cpu_id not in [0, 1, 2]:
|
|
1215
|
+
raise ValueError("Invalid ISR CPU ID.")
|
|
1216
|
+
|
|
1217
|
+
spi_config_data_temp = []
|
|
1218
|
+
spi_config_data_temp.extend(self.__data_decompose(mosi_io_num))
|
|
1219
|
+
spi_config_data_temp.extend(self.__data_decompose(miso_io_num))
|
|
1220
|
+
spi_config_data_temp.extend(self.__data_decompose(sclk_io_num))
|
|
1221
|
+
spi_config_data_temp.extend(self.__data_decompose(quadwp_io_num))
|
|
1222
|
+
spi_config_data_temp.extend(self.__data_decompose(quadhd_io_num))
|
|
1223
|
+
spi_config_data_temp.extend(self.__data_decompose(data4_io_num))
|
|
1224
|
+
spi_config_data_temp.extend(self.__data_decompose(data5_io_num))
|
|
1225
|
+
spi_config_data_temp.extend(self.__data_decompose(data6_io_num))
|
|
1226
|
+
spi_config_data_temp.extend(self.__data_decompose(data7_io_num))
|
|
1227
|
+
spi_config_data_temp.extend(self.__data_decompose(max_transfer_sz, 2))
|
|
1228
|
+
spi_config_data_temp.extend(self.__data_decompose(common_bus_flags))
|
|
1229
|
+
spi_config_data_temp.extend(self.__data_decompose(isr_cpu_id, 1))
|
|
1230
|
+
spi_config_data_temp.extend(self.__data_decompose(intr_flags))
|
|
1231
|
+
spi_config_data_temp.extend(self.__data_decompose(command_bits, 1))
|
|
1232
|
+
spi_config_data_temp.extend(self.__data_decompose(address_bits, 1))
|
|
1233
|
+
spi_config_data_temp.extend(self.__data_decompose(dummy_bits, 1))
|
|
1234
|
+
spi_config_data_temp.extend(self.__data_decompose(mode, 1))
|
|
1235
|
+
spi_config_data_temp.extend(self.__data_decompose(duty_cycle_pos, 2))
|
|
1236
|
+
spi_config_data_temp.extend(self.__data_decompose(cs_ena_pretrans, 2))
|
|
1237
|
+
spi_config_data_temp.extend(self.__data_decompose(cs_ena_posttrans, 1))
|
|
1238
|
+
spi_config_data_temp.extend(self.__data_decompose(freq))
|
|
1239
|
+
spi_config_data_temp.extend(self.__data_decompose(input_delay_ns))
|
|
1240
|
+
spi_config_data_temp.extend(self.__data_decompose(cs_io_num))
|
|
1241
|
+
spi_config_data_temp.extend(self.__data_decompose(device_interface_flags))
|
|
1242
|
+
spi_config_data_temp.extend(self.__data_decompose(queue_size))
|
|
1243
|
+
|
|
1244
|
+
self.__hexdump(spi_config_data_temp)
|
|
1245
|
+
|
|
1246
|
+
ret, _data = self.__task_execute(
|
|
1247
|
+
self.task_cmd["TASK_SPI_CONFIG"], spi_config_data_temp
|
|
1248
|
+
)
|
|
1249
|
+
self.__check_ret_code(self.task_cmd["TASK_SPI_CONFIG"], ret)
|
|
1250
|
+
|
|
1251
|
+
if ret == 0:
|
|
1252
|
+
for pin in self.__spi_used_pins:
|
|
1253
|
+
self.mark_pin_free(pin)
|
|
1254
|
+
for pin in [
|
|
1255
|
+
mosi_io_num,
|
|
1256
|
+
miso_io_num,
|
|
1257
|
+
sclk_io_num,
|
|
1258
|
+
cs_io_num,
|
|
1259
|
+
quadhd_io_num,
|
|
1260
|
+
quadwp_io_num,
|
|
1261
|
+
data4_io_num,
|
|
1262
|
+
data5_io_num,
|
|
1263
|
+
data6_io_num,
|
|
1264
|
+
data7_io_num,
|
|
1265
|
+
]:
|
|
1266
|
+
self.__mark_spi_used(pin)
|
|
1267
|
+
return True, None
|
|
1268
|
+
else:
|
|
1269
|
+
return False, None
|
|
1270
|
+
|
|
1271
|
+
def spi_read_image(self) -> tuple[bool, bytearray | None]:
|
|
1272
|
+
"""
|
|
1273
|
+
@brief Read image data from SPI slave device.
|
|
1274
|
+
@return: Image data.
|
|
1275
|
+
"""
|
|
1276
|
+
|
|
1277
|
+
ret, data = self.__task_execute(self.task_cmd["TASK_SPI_READ_IMAGE"], [])
|
|
1278
|
+
|
|
1279
|
+
if ret == 0:
|
|
1280
|
+
return True, data
|
|
1281
|
+
else:
|
|
1282
|
+
return False, None
|
|
1283
|
+
|
|
1284
|
+
def pwm_run_stop(
|
|
1285
|
+
self, pwm_running_state: bool, channel: int = 0
|
|
1286
|
+
) -> bool | tuple[bool, int]:
|
|
1287
|
+
"""
|
|
1288
|
+
@brief Run or stop PWM. It will generate a PWM signal with 10KHz frequency and 25% duty cycle in default.
|
|
1289
|
+
@param pwm_running_state: True to run PWM, False to stop PWM.
|
|
1290
|
+
@param channel: PWM channel. Default is 0. Available channels are 0, 1, 2.
|
|
1291
|
+
@return: Return True if success, False otherwise.
|
|
1292
|
+
"""
|
|
1293
|
+
|
|
1294
|
+
if channel not in [0, 1, 2]:
|
|
1295
|
+
logger.error("Invalid channel number. Available channels are 0, 1, 2.")
|
|
1296
|
+
return False
|
|
1297
|
+
|
|
1298
|
+
channel = ctypes.c_uint8(channel).value
|
|
1299
|
+
run_state = ctypes.c_uint8(pwm_running_state).value
|
|
1300
|
+
ret, _data = self.__task_execute(
|
|
1301
|
+
self.task_cmd["TASK_PWM_RUN_STOP"], [channel, run_state]
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
self.__check_ret_code(self.task_cmd["TASK_PWM_RUN_STOP"], ret)
|
|
1305
|
+
|
|
1306
|
+
if pwm_running_state:
|
|
1307
|
+
self.__pwm_states[channel] = True
|
|
1308
|
+
else:
|
|
1309
|
+
self.__pwm_states[channel] = False
|
|
1310
|
+
|
|
1311
|
+
return self.__pwm_states[channel], ret
|
|
1312
|
+
|
|
1313
|
+
def pwm_config(
|
|
1314
|
+
self,
|
|
1315
|
+
pin: int = 16,
|
|
1316
|
+
freq: int = 10000,
|
|
1317
|
+
duty: float = 0.5,
|
|
1318
|
+
channel: int = 0,
|
|
1319
|
+
resolution_hz: int = 80000000,
|
|
1320
|
+
) -> tuple[bool, None]:
|
|
1321
|
+
"""
|
|
1322
|
+
@brief Configure PWM.
|
|
1323
|
+
@param pin: PWM pin number. (Channel 0: `16` (in default); Channel 1: `17` (in default); Channel 2: `18` (in default).)
|
|
1324
|
+
@param freq: PWM frequency. Unit: Hz; (Default is `10000` (10KHz). Frequency should be far less than timer resolution.)
|
|
1325
|
+
@param duty: PWM duty cycle. (Default is `0.5` (50%). Duty cycle should be in the range of `0.0` (0%) to `1.0` (100%).)
|
|
1326
|
+
@param channel: PWM channel. (Default is `0`. Available channels are `0`, `1`, `2`.)
|
|
1327
|
+
@return: Return True if success, False otherwise.
|
|
1328
|
+
"""
|
|
1329
|
+
|
|
1330
|
+
self.__pwm_valid_pins = None
|
|
1331
|
+
|
|
1332
|
+
if pin not in self.pwm_valid_pins:
|
|
1333
|
+
logger.error("Invalid pin number.")
|
|
1334
|
+
return False, None
|
|
1335
|
+
|
|
1336
|
+
if duty < 0 or duty > 1:
|
|
1337
|
+
logger.error(
|
|
1338
|
+
"Invalid duty cycle. Duty cycle should be in the range of 0.0 to 1.0"
|
|
1339
|
+
)
|
|
1340
|
+
return False, None
|
|
1341
|
+
|
|
1342
|
+
if resolution_hz > 80_000_000:
|
|
1343
|
+
logger.error(
|
|
1344
|
+
"Invalid timer resolution. Timer resolution should be less than 80MHz."
|
|
1345
|
+
)
|
|
1346
|
+
return False, None
|
|
1347
|
+
else:
|
|
1348
|
+
coeff = 80_000_000 // resolution_hz
|
|
1349
|
+
|
|
1350
|
+
timer_resolution = resolution_hz
|
|
1351
|
+
|
|
1352
|
+
if freq > timer_resolution:
|
|
1353
|
+
logger.error(
|
|
1354
|
+
"Invalid frequency. Frequency should be less than timer resolution."
|
|
1355
|
+
)
|
|
1356
|
+
return False, None
|
|
1357
|
+
else:
|
|
1358
|
+
freq /= coeff
|
|
1359
|
+
freq = int(freq)
|
|
1360
|
+
|
|
1361
|
+
timer_resolution = ctypes.c_uint32(timer_resolution).value
|
|
1362
|
+
period_ticks = ctypes.c_uint32(resolution_hz // freq).value
|
|
1363
|
+
duty_ticks = int(period_ticks * duty)
|
|
1364
|
+
|
|
1365
|
+
if not (0 < period_ticks < 65535):
|
|
1366
|
+
raise ValueError(
|
|
1367
|
+
"Invalid period ticks. Should be in the range of 1 to 65535."
|
|
1368
|
+
)
|
|
1369
|
+
|
|
1370
|
+
pin = ctypes.c_uint8(pin).value
|
|
1371
|
+
channel = ctypes.c_uint8(channel).value
|
|
1372
|
+
|
|
1373
|
+
logger.debug(f"Period Ticks: {period_ticks}, Duty Ticks: {duty_ticks}")
|
|
1374
|
+
|
|
1375
|
+
pwm_data_temp = [
|
|
1376
|
+
(channel & 0xFF),
|
|
1377
|
+
(pin & 0xFF),
|
|
1378
|
+
(period_ticks & 0xFF000000) >> 24,
|
|
1379
|
+
(period_ticks & 0x00FF0000) >> 16,
|
|
1380
|
+
(period_ticks & 0x0000FF00) >> 8,
|
|
1381
|
+
period_ticks & 0x000000FF,
|
|
1382
|
+
(duty_ticks & 0xFF000000) >> 24,
|
|
1383
|
+
(duty_ticks & 0x00FF0000) >> 16,
|
|
1384
|
+
(duty_ticks & 0x0000FF00) >> 8,
|
|
1385
|
+
duty_ticks & 0x000000FF,
|
|
1386
|
+
(timer_resolution & 0xFF000000) >> 24,
|
|
1387
|
+
(timer_resolution & 0x00FF0000) >> 16,
|
|
1388
|
+
(timer_resolution & 0x0000FF00) >> 8,
|
|
1389
|
+
timer_resolution & 0x000000FF,
|
|
1390
|
+
]
|
|
1391
|
+
|
|
1392
|
+
ret, _ = self.__task_execute(self.task_cmd["TASK_PWM_CONFIG"], pwm_data_temp)
|
|
1393
|
+
|
|
1394
|
+
self.__check_ret_code(self.task_cmd["TASK_PWM_CONFIG"], ret)
|
|
1395
|
+
|
|
1396
|
+
if ret == 0:
|
|
1397
|
+
old_pins = list(self.__pwm_used_pins)
|
|
1398
|
+
for old_pin in old_pins:
|
|
1399
|
+
self.mark_pin_free(old_pin)
|
|
1400
|
+
self.__mark_pwm_used(pin)
|
|
1401
|
+
|
|
1402
|
+
if self.__pwm_states[channel]:
|
|
1403
|
+
self.pwm_run_stop(True, channel)
|
|
1404
|
+
|
|
1405
|
+
return True, None
|
|
1406
|
+
|
|
1407
|
+
def usb_config(self, crc_enable: bool = False) -> tuple[bool, None]:
|
|
1408
|
+
"""
|
|
1409
|
+
@brief Configure USB.
|
|
1410
|
+
@param crc_enable: (`True`: enable CRC, `False`: disable CRC.)
|
|
1411
|
+
@return: Return True if success, False otherwise.
|
|
1412
|
+
"""
|
|
1413
|
+
|
|
1414
|
+
crc_enable = ctypes.c_uint8(crc_enable).value
|
|
1415
|
+
self.__crc_enable = crc_enable
|
|
1416
|
+
|
|
1417
|
+
ret, _data = self.__task_execute(self.task_cmd["TASK_USB_CONFIG"], [crc_enable])
|
|
1418
|
+
|
|
1419
|
+
if ret == 0:
|
|
1420
|
+
return True, None
|
|
1421
|
+
else:
|
|
1422
|
+
return False, None
|
|
1423
|
+
|
|
1424
|
+
def power_init(self) -> bool:
|
|
1425
|
+
"""
|
|
1426
|
+
@brief Initialize power.
|
|
1427
|
+
@return: Return True if success, False otherwise.
|
|
1428
|
+
"""
|
|
1429
|
+
|
|
1430
|
+
if not self.__expand_io_init_status:
|
|
1431
|
+
ret = self.expand_io_init()
|
|
1432
|
+
if ret is not True:
|
|
1433
|
+
return False
|
|
1434
|
+
|
|
1435
|
+
if not self.__power_init_status:
|
|
1436
|
+
match self.__extboard_version:
|
|
1437
|
+
case "v0.1":
|
|
1438
|
+
# set IO1 to IO3 as output
|
|
1439
|
+
|
|
1440
|
+
ret, data = self.i2c_write_read(
|
|
1441
|
+
self.__pca9557pw_addr, [0x03], 1, port=1
|
|
1442
|
+
)
|
|
1443
|
+
if ret is not True:
|
|
1444
|
+
logger.error("PCA9557PW read failed.")
|
|
1445
|
+
raise ValueError("PCA9557PW read failed.")
|
|
1446
|
+
|
|
1447
|
+
bit_mask = data[0]
|
|
1448
|
+
bit_mask &= 0xE1 # 1110 0001
|
|
1449
|
+
|
|
1450
|
+
ret, data = self.i2c_write_read(
|
|
1451
|
+
self.__pca9557pw_addr, [0x03, bit_mask], 0, port=1
|
|
1452
|
+
)
|
|
1453
|
+
if ret is not True:
|
|
1454
|
+
logger.error("PCA9557PW write failed.")
|
|
1455
|
+
raise ValueError("PCA9557PW write failed.")
|
|
1456
|
+
|
|
1457
|
+
self.__expand_io_mode_bitmask = bit_mask
|
|
1458
|
+
self.__power_init_status = True
|
|
1459
|
+
|
|
1460
|
+
case "v0.2.1":
|
|
1461
|
+
ret, data = self.i2c_write_read(
|
|
1462
|
+
self.__tca9555pwr_addr, [0x06], 2, port=1
|
|
1463
|
+
)
|
|
1464
|
+
if ret is not True:
|
|
1465
|
+
logger.error("TCA9555PWR read failed.")
|
|
1466
|
+
raise ValueError("TCA9555PWR read failed.")
|
|
1467
|
+
|
|
1468
|
+
bit_mask = data[1] << 8 | data[0]
|
|
1469
|
+
bit_mask &= 0xFC00 # 1111 1100 0000 0000
|
|
1470
|
+
|
|
1471
|
+
ret, data = self.i2c_write_read(
|
|
1472
|
+
self.__tca9555pwr_addr,
|
|
1473
|
+
[0x06, bit_mask & 0x00FF, (bit_mask & 0xFF00) >> 8],
|
|
1474
|
+
0,
|
|
1475
|
+
port=1,
|
|
1476
|
+
)
|
|
1477
|
+
if ret is not True:
|
|
1478
|
+
logger.error("TCA9555PWR write failed.")
|
|
1479
|
+
raise ValueError("TCA9555PWR write failed.")
|
|
1480
|
+
|
|
1481
|
+
self.__expand_io_mode_bitmask = bit_mask
|
|
1482
|
+
self.__power_init_status = True
|
|
1483
|
+
|
|
1484
|
+
case _:
|
|
1485
|
+
logger.error("Invalid extension board version.")
|
|
1486
|
+
raise ValueError("Invalid extension board version.")
|
|
1487
|
+
|
|
1488
|
+
else:
|
|
1489
|
+
logger.info("Power already initialized.")
|
|
1490
|
+
|
|
1491
|
+
return True
|
|
1492
|
+
|
|
1493
|
+
def power_control(
|
|
1494
|
+
self, communication_type: str = "SPI", power_type: str = "1V8"
|
|
1495
|
+
) -> bool:
|
|
1496
|
+
"""
|
|
1497
|
+
@brief Control power for communication type.
|
|
1498
|
+
@param communication_type: Communication types which are supported to use level shifter. (`SPI` or `I2C` are available.)
|
|
1499
|
+
@param power_type: Power types which are supported to swich power. (`1V8`, `1V2` or `0V` are supported in ***Extboard v0.1***, while `3V3`, `1V8`, `1V2` or `0V` are supported in ***Extboard v0.2.1***.)
|
|
1500
|
+
@return: Return True if success, False otherwise.
|
|
1501
|
+
"""
|
|
1502
|
+
|
|
1503
|
+
communication_type = communication_type.upper()
|
|
1504
|
+
power_type = power_type.upper()
|
|
1505
|
+
|
|
1506
|
+
match self.__extboard_version:
|
|
1507
|
+
case "v0.1":
|
|
1508
|
+
if communication_type not in ["SPI", "I2C"]:
|
|
1509
|
+
logger.error(
|
|
1510
|
+
"Invalid communication type. Only SPI and I2C are supported."
|
|
1511
|
+
)
|
|
1512
|
+
return False
|
|
1513
|
+
|
|
1514
|
+
if power_type not in ["1V8", "1V2", "0V"]:
|
|
1515
|
+
logger.error(
|
|
1516
|
+
"Invalid power type. Only 1V8, 1V2 and 0V are supported."
|
|
1517
|
+
)
|
|
1518
|
+
return False
|
|
1519
|
+
|
|
1520
|
+
ret, data = self.i2c_write_read(
|
|
1521
|
+
self.__pca9557pw_addr, [0x01], 1, port=1
|
|
1522
|
+
)
|
|
1523
|
+
if ret is not True:
|
|
1524
|
+
logger.error("PCA9557PW read failed.")
|
|
1525
|
+
raise ValueError("PCA9557PW read failed.")
|
|
1526
|
+
|
|
1527
|
+
bit_mask = data[0]
|
|
1528
|
+
|
|
1529
|
+
match communication_type:
|
|
1530
|
+
case "SPI":
|
|
1531
|
+
match power_type:
|
|
1532
|
+
case "1V8": # IO3, bit 3
|
|
1533
|
+
bit_mask |= 0x08 # 0000 1000
|
|
1534
|
+
bit_mask &= 0xFD # 1111 1101
|
|
1535
|
+
|
|
1536
|
+
case "1V2": # IO1, bit 1
|
|
1537
|
+
bit_mask |= 0x02 # 0000 0010
|
|
1538
|
+
bit_mask &= 0xF7 # 1111 0111
|
|
1539
|
+
|
|
1540
|
+
case "0V": # set IO3 and IO1 to 0
|
|
1541
|
+
bit_mask &= 0xF5 # 1111 0101
|
|
1542
|
+
|
|
1543
|
+
case "3V3":
|
|
1544
|
+
logger.error(
|
|
1545
|
+
"Invalid power type. 3V3 is not supported in Extboard v0.1."
|
|
1546
|
+
)
|
|
1547
|
+
return False
|
|
1548
|
+
|
|
1549
|
+
case _:
|
|
1550
|
+
logger.error(
|
|
1551
|
+
"Invalid power type. Only 1V8, 1V2 and 0V are supported."
|
|
1552
|
+
)
|
|
1553
|
+
return False
|
|
1554
|
+
|
|
1555
|
+
case "I2C":
|
|
1556
|
+
match power_type:
|
|
1557
|
+
case "1V8": # IO4, bit 4
|
|
1558
|
+
bit_mask |= 0x10 # 0001 0000
|
|
1559
|
+
bit_mask &= 0xFB # 1111 1011
|
|
1560
|
+
|
|
1561
|
+
case "1V2": # IO2, bit 2
|
|
1562
|
+
bit_mask |= 0x04 # 0000 0100
|
|
1563
|
+
bit_mask &= 0xEF # 1110 1111
|
|
1564
|
+
|
|
1565
|
+
case "0V": # set IO4 and IO2 to 0
|
|
1566
|
+
bit_mask &= 0xEB # 1110 1011
|
|
1567
|
+
|
|
1568
|
+
case "3V3":
|
|
1569
|
+
logger.error(
|
|
1570
|
+
"Invalid power type. 3V3 is not supported in Extboard v0.1."
|
|
1571
|
+
)
|
|
1572
|
+
return False
|
|
1573
|
+
|
|
1574
|
+
case _:
|
|
1575
|
+
logger.error(
|
|
1576
|
+
"Invalid power type. Only 1V8, 1V2 and 0V are supported."
|
|
1577
|
+
)
|
|
1578
|
+
return False
|
|
1579
|
+
|
|
1580
|
+
ret, data = self.i2c_write_read(
|
|
1581
|
+
self.__pca9557pw_addr, [0x01, bit_mask], 0, port=1
|
|
1582
|
+
)
|
|
1583
|
+
if ret is not True:
|
|
1584
|
+
logger.error("PCA9557PW write failed.")
|
|
1585
|
+
raise ValueError("PCA9557PW write failed.")
|
|
1586
|
+
|
|
1587
|
+
case "v0.2.1":
|
|
1588
|
+
if power_type not in ["3V3", "1V8", "1V2", "0V"]:
|
|
1589
|
+
logger.error(
|
|
1590
|
+
"Invalid power type. Only 3V3, 1V8, 1V2 and 0V are supported."
|
|
1591
|
+
)
|
|
1592
|
+
return False
|
|
1593
|
+
|
|
1594
|
+
ret, data = self.i2c_write_read(
|
|
1595
|
+
self.__tca9555pwr_addr, [0x02], 2, port=1
|
|
1596
|
+
)
|
|
1597
|
+
if ret is not True:
|
|
1598
|
+
logger.error("TCA9555PWR read failed.")
|
|
1599
|
+
raise ValueError("TCA9555PWR read failed.")
|
|
1600
|
+
|
|
1601
|
+
bit_mask = data[1] << 8 | data[0]
|
|
1602
|
+
|
|
1603
|
+
"""
|
|
1604
|
+
REG 0x02
|
|
1605
|
+
| O 0.7 | O 0.6 | O 0.5 | O 0.4 | O 0.3 | O 0.2 | O 0.1 | O 0.0 |
|
|
1606
|
+
|-------|-------|---------|---------|---------|---------|---------|---------|
|
|
1607
|
+
| 1V8_2 | 1V8_1 | 3V3_I2C | 1V8_I2C | 1V2_I2C | 3V3_SPI | 1V8_SPI | 1V2_SPI |
|
|
1608
|
+
|
|
1609
|
+
REG 0x03
|
|
1610
|
+
| O 1.7 | O 1.6 | O 1.5 | O 1.4 | O 1.3 | O 1.2 | O 1.1 | O 1.0 |
|
|
1611
|
+
|-------|-------|-------|-------|-------|-------|-------|-------|
|
|
1612
|
+
| IO17 | IO16 | IO15 | IO14 | IO13 | IO12 | 3V3_2 | 3V3_1 |
|
|
1613
|
+
"""
|
|
1614
|
+
|
|
1615
|
+
match communication_type:
|
|
1616
|
+
case "SPI":
|
|
1617
|
+
match power_type:
|
|
1618
|
+
case "3V3":
|
|
1619
|
+
bit_mask |= 0x0004 # 0000 0000 0000 0100
|
|
1620
|
+
bit_mask &= 0xFFFC # 1111 1111 1111 1100
|
|
1621
|
+
|
|
1622
|
+
case "1V8":
|
|
1623
|
+
bit_mask |= 0x0002 # 0000 0000 0000 0010
|
|
1624
|
+
bit_mask &= 0xFFFA # 1111 1111 1111 1010
|
|
1625
|
+
|
|
1626
|
+
case "1V2":
|
|
1627
|
+
bit_mask |= 0x0001 # 0000 0000 0000 0001
|
|
1628
|
+
bit_mask &= 0xFFF9 # 1111 1111 1111 1001
|
|
1629
|
+
|
|
1630
|
+
case "0V":
|
|
1631
|
+
bit_mask &= 0xFFF8 # 1111 1111 1111 1000
|
|
1632
|
+
|
|
1633
|
+
case _:
|
|
1634
|
+
logger.error(
|
|
1635
|
+
"Invalid power type. Only 3V3, 1V8, 1V2 and 0V are supported."
|
|
1636
|
+
)
|
|
1637
|
+
return False
|
|
1638
|
+
|
|
1639
|
+
case "I2C":
|
|
1640
|
+
match power_type:
|
|
1641
|
+
case "3V3":
|
|
1642
|
+
bit_mask |= 0x0020 # 0000 0000 0010 0000
|
|
1643
|
+
bit_mask &= 0xFFE7 # 1111 1111 1110 0111
|
|
1644
|
+
|
|
1645
|
+
case "1V8":
|
|
1646
|
+
bit_mask |= 0x0010 # 0000 0000 0001 0000
|
|
1647
|
+
bit_mask &= 0xFFD7 # 1111 1111 1101 0111
|
|
1648
|
+
|
|
1649
|
+
case "1V2":
|
|
1650
|
+
bit_mask |= 0x0008 # 0000 0000 0000 1000
|
|
1651
|
+
bit_mask &= 0xFFCF # 1111 1111 1100 1111
|
|
1652
|
+
|
|
1653
|
+
case "0V":
|
|
1654
|
+
bit_mask &= 0xFFC7 # 1111 1111 1100 0111
|
|
1655
|
+
|
|
1656
|
+
case _:
|
|
1657
|
+
logger.error(
|
|
1658
|
+
"Invalid power type. Only 3V3, 1V8, 1V2 and 0V are supported."
|
|
1659
|
+
)
|
|
1660
|
+
return False
|
|
1661
|
+
|
|
1662
|
+
case _:
|
|
1663
|
+
logger.error(
|
|
1664
|
+
"Invalid communication type. Only SPI and I2C are supported."
|
|
1665
|
+
)
|
|
1666
|
+
return False
|
|
1667
|
+
|
|
1668
|
+
ret, data = self.i2c_write_read(
|
|
1669
|
+
self.__tca9555pwr_addr,
|
|
1670
|
+
[0x02, bit_mask & 0x00FF, (bit_mask & 0xFF00) >> 8],
|
|
1671
|
+
0,
|
|
1672
|
+
port=1,
|
|
1673
|
+
)
|
|
1674
|
+
if ret is not True:
|
|
1675
|
+
logger.error("TCA9555PWR write failed.")
|
|
1676
|
+
raise ValueError("TCA9555PWR write failed.")
|
|
1677
|
+
|
|
1678
|
+
case _:
|
|
1679
|
+
logger.error("Invalid extension board version.")
|
|
1680
|
+
raise ValueError("Invalid extension board version.")
|
|
1681
|
+
|
|
1682
|
+
return True
|
|
1683
|
+
|
|
1684
|
+
def expand_io_init(self) -> bool:
|
|
1685
|
+
"""
|
|
1686
|
+
@brief Initialize expand IO.
|
|
1687
|
+
@return: Return True if success, False otherwise.
|
|
1688
|
+
"""
|
|
1689
|
+
|
|
1690
|
+
ret, board_version = self.get_extboard_version()
|
|
1691
|
+
if ret is not True:
|
|
1692
|
+
return False
|
|
1693
|
+
if board_version is None:
|
|
1694
|
+
return False
|
|
1695
|
+
|
|
1696
|
+
if not self.__expand_io_init_status:
|
|
1697
|
+
if board_version == "v0.1":
|
|
1698
|
+
reg_list = [
|
|
1699
|
+
[0x02, 0x00], # set all pins as normal polarity
|
|
1700
|
+
[0x03, 0xFF], # set all pins as input mode
|
|
1701
|
+
[0x01, 0x00], # set all pins as low level
|
|
1702
|
+
]
|
|
1703
|
+
|
|
1704
|
+
for reg in reg_list:
|
|
1705
|
+
ret, _data = self.i2c_write_read(
|
|
1706
|
+
self.__pca9557pw_addr, reg, 0, port=1
|
|
1707
|
+
)
|
|
1708
|
+
if not ret:
|
|
1709
|
+
logger.error("PCA9557PW init failed.")
|
|
1710
|
+
raise ValueError("PCA9557PW init failed.")
|
|
1711
|
+
|
|
1712
|
+
self.__expand_io_mode_bitmask = 0xFF
|
|
1713
|
+
|
|
1714
|
+
elif board_version == "v0.2.1":
|
|
1715
|
+
reg_list = [
|
|
1716
|
+
[0x06, 0xFF, 0xFF], # set all pins as input mode
|
|
1717
|
+
[0x04, 0x00, 0x00], # disable polarity inversion
|
|
1718
|
+
[0x02, 0x00, 0x00], # set all pins as low level
|
|
1719
|
+
]
|
|
1720
|
+
|
|
1721
|
+
for reg_data in reg_list:
|
|
1722
|
+
_ret, _data = self.i2c_write_read(
|
|
1723
|
+
self.__tca9555pwr_addr, reg_data, 0, port=1
|
|
1724
|
+
)
|
|
1725
|
+
if not _ret:
|
|
1726
|
+
logger.error("TCA9555PWR init failed.")
|
|
1727
|
+
raise ValueError("TCA9555PWR init failed.")
|
|
1728
|
+
|
|
1729
|
+
self.__expand_io_mode_bitmask = 0xFFFF
|
|
1730
|
+
|
|
1731
|
+
self.__expand_io_init_status = True
|
|
1732
|
+
return True
|
|
1733
|
+
|
|
1734
|
+
def expand_io_config(self, pin: int, mode: int) -> bool:
|
|
1735
|
+
"""
|
|
1736
|
+
@brief Configure expand IO pin mode.
|
|
1737
|
+
@param pin: Expanded pin number on ExtBoard. (`IO0`, `IO5`, `IO6`, `IO7` are supported in ***Extboard v0.1***, while `IO6` (`1V8_1`), `IO7` (`1V8_2`), `IO10`(`3V3_1`), `IO11`(`3V3_2`), `IO12`, `IO13`, `IO14`, `IO15`, `IO16`, `IO17` are supported in ***Extboard v0.2.1***.)
|
|
1738
|
+
@param mode: Pin mode. `0`: Input, `1`: Output.
|
|
1739
|
+
"""
|
|
1740
|
+
|
|
1741
|
+
if mode not in [0, 1]:
|
|
1742
|
+
raise ValueError("Invalid mode. Only 0 and 1 are supported.")
|
|
1743
|
+
|
|
1744
|
+
if not self.__expand_io_init_status:
|
|
1745
|
+
self.expand_io_init()
|
|
1746
|
+
|
|
1747
|
+
match self.__extboard_version:
|
|
1748
|
+
case "v0.1":
|
|
1749
|
+
if pin not in [0, 5, 6, 7]:
|
|
1750
|
+
raise ValueError(
|
|
1751
|
+
"Invalid pin number. Only 0, 5, 6, 7 are supported."
|
|
1752
|
+
)
|
|
1753
|
+
|
|
1754
|
+
ret, data = self.i2c_write_read(
|
|
1755
|
+
self.__pca9557pw_addr, [0x03], 1, port=1
|
|
1756
|
+
)
|
|
1757
|
+
if ret is not True:
|
|
1758
|
+
logger.error("PCA9557PW read failed.")
|
|
1759
|
+
raise ValueError("PCA9557PW read failed.")
|
|
1760
|
+
|
|
1761
|
+
bit_mask = data[0]
|
|
1762
|
+
bit_mask = (
|
|
1763
|
+
(bit_mask & ~(1 << pin)) if mode == 1 else (bit_mask | (1 << pin))
|
|
1764
|
+
)
|
|
1765
|
+
|
|
1766
|
+
ret, data = self.i2c_write_read(
|
|
1767
|
+
self.__pca9557pw_addr, [0x03, bit_mask], 0, port=1
|
|
1768
|
+
)
|
|
1769
|
+
if ret is not True:
|
|
1770
|
+
logger.error("PCA9557PW write failed.")
|
|
1771
|
+
raise ValueError("PCA9557PW write failed.")
|
|
1772
|
+
|
|
1773
|
+
self.__expand_io_mode_bitmask = bit_mask
|
|
1774
|
+
|
|
1775
|
+
case "v0.2.1":
|
|
1776
|
+
"""
|
|
1777
|
+
REG 0x02
|
|
1778
|
+
| O 0.7 | O 0.6 | O 0.5 | O 0.4 | O 0.3 | O 0.2 | O 0.1 | O 0.0 |
|
|
1779
|
+
|------------|------------|---------|---------|---------|---------|---------|---------|
|
|
1780
|
+
| (IO7)1V8_2 | (IO6)1V8_1 | 3V3_I2C | 1V8_I2C | 1V2_I2C | 3V3_SPI | 1V8_SPI | 1V2_SPI |
|
|
1781
|
+
|
|
1782
|
+
REG 0x03
|
|
1783
|
+
| O 1.7 | O 1.6 | O 1.5 | O 1.4 | O 1.3 | O 1.2 | O 1.1 | O 1.0 |
|
|
1784
|
+
|-------|-------|-------|-------|-------|-------|-------|-------|
|
|
1785
|
+
| IO17 | IO16 | IO15 | IO14 | IO13 | IO12 | 3V3_2 | 3V3_1 |
|
|
1786
|
+
"""
|
|
1787
|
+
|
|
1788
|
+
if pin not in [12, 13, 14, 15, 16, 17]:
|
|
1789
|
+
raise ValueError(
|
|
1790
|
+
"Invalid pin number. Only 12, 13, 14, 15, 16, 17 are supported."
|
|
1791
|
+
)
|
|
1792
|
+
|
|
1793
|
+
ret, data = self.i2c_write_read(
|
|
1794
|
+
self.__tca9555pwr_addr, [0x06], 2, port=1
|
|
1795
|
+
)
|
|
1796
|
+
if ret is not True:
|
|
1797
|
+
logger.error("TCA9555PWR read failed.")
|
|
1798
|
+
raise ValueError("TCA9555PWR read failed.")
|
|
1799
|
+
|
|
1800
|
+
bit_mask = data[1] << 8 | data[0]
|
|
1801
|
+
if pin > 7:
|
|
1802
|
+
pin -= 2
|
|
1803
|
+
|
|
1804
|
+
bit_mask = (
|
|
1805
|
+
(bit_mask & ~(1 << pin)) if mode == 1 else (bit_mask | (1 << pin))
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
ret, data = self.i2c_write_read(
|
|
1809
|
+
self.__tca9555pwr_addr,
|
|
1810
|
+
[0x06, bit_mask & 0x00FF, (bit_mask & 0xFF00) >> 8],
|
|
1811
|
+
0,
|
|
1812
|
+
port=1,
|
|
1813
|
+
)
|
|
1814
|
+
if ret is not True:
|
|
1815
|
+
logger.error("TCA9555PWR write failed.")
|
|
1816
|
+
raise ValueError("TCA9555PWR write failed.")
|
|
1817
|
+
|
|
1818
|
+
self.__expand_io_mode_bitmask = bit_mask
|
|
1819
|
+
|
|
1820
|
+
case _:
|
|
1821
|
+
logger.error("Invalid extension board version.")
|
|
1822
|
+
raise ValueError("Invalid extension board version.")
|
|
1823
|
+
|
|
1824
|
+
return True
|
|
1825
|
+
|
|
1826
|
+
def expand_io_write_read(
|
|
1827
|
+
self, pin: int, level: int | None = None
|
|
1828
|
+
) -> tuple[bool, int | None]:
|
|
1829
|
+
"""
|
|
1830
|
+
@brief Write or read level of expand IO pin.
|
|
1831
|
+
@param pin: Expanded pin number on ExtBoard. (`IO0`, `IO5`, `IO6`, `IO7` are supported in ***Extboard v0.1***, while `IO6` (`1V8_1`), `IO7` (`1V8_2`), `IO10`(`3V3_1`), `IO11`(`3V3_2`), `IO12`, `IO13`, `IO14`, `IO15`, `IO16`, `IO17` are supported in ***Extboard v0.2.1***.)
|
|
1832
|
+
@param level: Level to write. `0`: Low, `1`: High. None in default for read mode.
|
|
1833
|
+
"""
|
|
1834
|
+
|
|
1835
|
+
if level not in [0, 1] and level is not None:
|
|
1836
|
+
raise ValueError("Invalid level. Only 0 and 1 are supported.")
|
|
1837
|
+
|
|
1838
|
+
match self.__extboard_version:
|
|
1839
|
+
case "v0.1":
|
|
1840
|
+
expand_pin_mapping = {
|
|
1841
|
+
"IO0": 0,
|
|
1842
|
+
"IO5": 5,
|
|
1843
|
+
"IO6": 6,
|
|
1844
|
+
"IO7": 7,
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
if isinstance(pin, str):
|
|
1848
|
+
pin = pin.upper()
|
|
1849
|
+
|
|
1850
|
+
if pin not in expand_pin_mapping:
|
|
1851
|
+
raise ValueError(
|
|
1852
|
+
"Invalid pin number. Only IO0, IO5, IO6, IO7 are supported."
|
|
1853
|
+
)
|
|
1854
|
+
else:
|
|
1855
|
+
pin = expand_pin_mapping[pin]
|
|
1856
|
+
|
|
1857
|
+
if pin not in [0, 5, 6, 7]:
|
|
1858
|
+
raise ValueError(
|
|
1859
|
+
"Invalid pin number. Only 0, 5, 6, 7 are supported."
|
|
1860
|
+
)
|
|
1861
|
+
|
|
1862
|
+
# read level
|
|
1863
|
+
if level is None:
|
|
1864
|
+
ret, data = self.i2c_write_read(
|
|
1865
|
+
self.__pca9557pw_addr,
|
|
1866
|
+
[0x00]
|
|
1867
|
+
if (self.__expand_io_mode_bitmask >> pin) & 0x01 == 1
|
|
1868
|
+
else [0x01],
|
|
1869
|
+
1,
|
|
1870
|
+
port=1,
|
|
1871
|
+
)
|
|
1872
|
+
if ret is not True:
|
|
1873
|
+
logger.error("PCA9557PW read failed.")
|
|
1874
|
+
raise ValueError("PCA9557PW read failed.")
|
|
1875
|
+
|
|
1876
|
+
level_ = (data[0] >> pin) & 0x01
|
|
1877
|
+
|
|
1878
|
+
return True, level_
|
|
1879
|
+
|
|
1880
|
+
# write level
|
|
1881
|
+
else:
|
|
1882
|
+
if self.__expand_io_mode_bitmask >> pin & 0x01 == 1:
|
|
1883
|
+
raise ValueError(
|
|
1884
|
+
"Pin is in input mode. Use expand_io_config() to set pin to output mode first."
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
ret, data = self.i2c_write_read(
|
|
1888
|
+
self.__pca9557pw_addr, [0x00], 1, port=1
|
|
1889
|
+
)
|
|
1890
|
+
if ret is not True:
|
|
1891
|
+
logger.error("PCA9557PW read failed.")
|
|
1892
|
+
raise ValueError("PCA9557PW read failed.")
|
|
1893
|
+
|
|
1894
|
+
if level == 1:
|
|
1895
|
+
bit_mask = data[0] | (1 << pin)
|
|
1896
|
+
else:
|
|
1897
|
+
bit_mask = data[0] & ~(1 << pin)
|
|
1898
|
+
|
|
1899
|
+
ret, data = self.i2c_write_read(
|
|
1900
|
+
self.__pca9557pw_addr, [0x01, bit_mask], 0, port=1
|
|
1901
|
+
)
|
|
1902
|
+
if ret is not True:
|
|
1903
|
+
logger.error("PCA9557PW write failed.")
|
|
1904
|
+
raise ValueError("PCA9557PW write failed.")
|
|
1905
|
+
|
|
1906
|
+
return True, None
|
|
1907
|
+
|
|
1908
|
+
case "v0.2.1":
|
|
1909
|
+
expand_pin_mapping = {
|
|
1910
|
+
"IO6": 6,
|
|
1911
|
+
"IO7": 7,
|
|
1912
|
+
"IO10": 10,
|
|
1913
|
+
"IO11": 11,
|
|
1914
|
+
"IO12": 12,
|
|
1915
|
+
"IO13": 13,
|
|
1916
|
+
"IO14": 14,
|
|
1917
|
+
"IO15": 15,
|
|
1918
|
+
"IO16": 16,
|
|
1919
|
+
"IO17": 17,
|
|
1920
|
+
"1V8_1": 6,
|
|
1921
|
+
"1V8_2": 7,
|
|
1922
|
+
"3V3_1": 10,
|
|
1923
|
+
"3V3_2": 11,
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
if isinstance(pin, str):
|
|
1927
|
+
pin = pin.upper()
|
|
1928
|
+
|
|
1929
|
+
if pin not in expand_pin_mapping:
|
|
1930
|
+
raise ValueError(
|
|
1931
|
+
"Invalid pin number. Only IO6, IO7, IO10, IO11, IO12, IO13, IO14, IO15, IO16, IO17, 1V8_1, 1V8_2, 3V3_1, 3V3_2 are supported."
|
|
1932
|
+
)
|
|
1933
|
+
else:
|
|
1934
|
+
pin = expand_pin_mapping[pin]
|
|
1935
|
+
|
|
1936
|
+
if pin not in [6, 7, 10, 11, 12, 13, 14, 15, 16, 17]:
|
|
1937
|
+
raise ValueError(
|
|
1938
|
+
"Invalid pin number. Only 6, 7, 10, 11, 12, 13, 14, 15, 16, 17 are supported."
|
|
1939
|
+
)
|
|
1940
|
+
|
|
1941
|
+
if pin > 7:
|
|
1942
|
+
pin -= 2
|
|
1943
|
+
|
|
1944
|
+
# read level
|
|
1945
|
+
if level is None:
|
|
1946
|
+
ret, data = self.i2c_write_read(
|
|
1947
|
+
self.__tca9555pwr_addr, [0x00], 2, port=1
|
|
1948
|
+
)
|
|
1949
|
+
if ret is not True:
|
|
1950
|
+
logger.error("TCA9555PWR read failed.")
|
|
1951
|
+
raise ValueError("TCA9555PWR read failed.")
|
|
1952
|
+
|
|
1953
|
+
level_ = (data[1] << 8 | data[0]) >> pin & 0x01
|
|
1954
|
+
|
|
1955
|
+
return True, level_
|
|
1956
|
+
|
|
1957
|
+
# write level
|
|
1958
|
+
else:
|
|
1959
|
+
if self.__expand_io_mode_bitmask >> pin & 0x01 == 1:
|
|
1960
|
+
raise ValueError(
|
|
1961
|
+
"Pin is in input mode. Use expand_io_config() to set pin to output mode first."
|
|
1962
|
+
)
|
|
1963
|
+
|
|
1964
|
+
ret, data = self.i2c_write_read(
|
|
1965
|
+
self.__tca9555pwr_addr, [0x00], 2, port=1
|
|
1966
|
+
)
|
|
1967
|
+
if ret is not True:
|
|
1968
|
+
logger.error("TCA9555PWR read failed.")
|
|
1969
|
+
raise ValueError("TCA9555PWR read failed.")
|
|
1970
|
+
|
|
1971
|
+
if level == 1:
|
|
1972
|
+
bit_mask = (data[1] << 8 | data[0]) | (1 << pin)
|
|
1973
|
+
else:
|
|
1974
|
+
bit_mask = (data[1] << 8 | data[0]) & ~(1 << pin)
|
|
1975
|
+
|
|
1976
|
+
ret, data = self.i2c_write_read(
|
|
1977
|
+
self.__tca9555pwr_addr,
|
|
1978
|
+
[0x02, bit_mask & 0x00FF, (bit_mask & 0xFF00) >> 8],
|
|
1979
|
+
0,
|
|
1980
|
+
port=1,
|
|
1981
|
+
)
|
|
1982
|
+
if ret is not True:
|
|
1983
|
+
logger.error("TCA9555PWR write failed.")
|
|
1984
|
+
raise ValueError("TCA9555PWR write failed.")
|
|
1985
|
+
|
|
1986
|
+
return True, None
|
|
1987
|
+
|
|
1988
|
+
case _:
|
|
1989
|
+
logger.error("Invalid extension board version.")
|
|
1990
|
+
raise ValueError("Invalid extension board version.")
|
|
1991
|
+
|
|
1992
|
+
def restart(self) -> bool:
|
|
1993
|
+
"""
|
|
1994
|
+
@brief Restart the device.
|
|
1995
|
+
"""
|
|
1996
|
+
|
|
1997
|
+
ret, _data = self.__task_execute(self.task_cmd["TASK_RESET_DEVICE"], [])
|
|
1998
|
+
if ret != 0:
|
|
1999
|
+
logger.error("Restart failed.")
|
|
2000
|
+
raise ValueError("Restart failed.")
|
|
2001
|
+
|
|
2002
|
+
return True
|
|
2003
|
+
|
|
2004
|
+
def get_extboard_version(self) -> tuple[bool, str | None]:
|
|
2005
|
+
"""
|
|
2006
|
+
@brief Get extension board version.
|
|
2007
|
+
@return: Extension board version.
|
|
2008
|
+
"""
|
|
2009
|
+
|
|
2010
|
+
ret, data = self.i2c_find_slave(port=1)
|
|
2011
|
+
if ret is not True:
|
|
2012
|
+
# Save original I2C1 pin config
|
|
2013
|
+
orig_sda = self.__i2c_used_pins[0] if len(self.__i2c_used_pins) > 0 else 10
|
|
2014
|
+
orig_scl = self.__i2c_used_pins[1] if len(self.__i2c_used_pins) > 1 else 11
|
|
2015
|
+
|
|
2016
|
+
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> BUG FIXED FOR v0.2.1 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
|
2017
|
+
ret, data = self.i2c_config(
|
|
2018
|
+
port=1,
|
|
2019
|
+
freq=400000,
|
|
2020
|
+
sda_pin=41,
|
|
2021
|
+
scl_pin=42,
|
|
2022
|
+
sda_pullup=False,
|
|
2023
|
+
scl_pullup=False,
|
|
2024
|
+
)
|
|
2025
|
+
logger.info("I2C config for extension board.")
|
|
2026
|
+
if ret is not True:
|
|
2027
|
+
logger.error("I2C config failed.")
|
|
2028
|
+
raise ValueError("I2C config failed.")
|
|
2029
|
+
|
|
2030
|
+
ret, data = self.i2c_find_slave(port=1)
|
|
2031
|
+
if ret is not True:
|
|
2032
|
+
logger.info("No extension board found.")
|
|
2033
|
+
# Restore original I2C1 pin config
|
|
2034
|
+
self.i2c_config(port=1, freq=400000, sda_pin=orig_sda, scl_pin=orig_scl)
|
|
2035
|
+
return False, None
|
|
2036
|
+
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> BUG FIXED FOR v0.2.1 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
|
2037
|
+
|
|
2038
|
+
if self.__pca9557pw_addr in [int(i, 16) for i in data]:
|
|
2039
|
+
self.__extboard_version = "v0.1"
|
|
2040
|
+
logger.info("Extension board version: v0.1")
|
|
2041
|
+
|
|
2042
|
+
elif self.__tca9555pwr_addr in [int(i, 16) for i in data]:
|
|
2043
|
+
self.__extboard_version = "v0.2.1"
|
|
2044
|
+
logger.info("Extension board version: v0.2.1")
|
|
2045
|
+
|
|
2046
|
+
else:
|
|
2047
|
+
logger.error("Unknown extension board.")
|
|
2048
|
+
return False, None
|
|
2049
|
+
|
|
2050
|
+
return True, self.__extboard_version
|