-
Notifications
You must be signed in to change notification settings - Fork 9
/
puyaisp.py
executable file
·392 lines (343 loc) · 13.3 KB
/
puyaisp.py
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
#!/usr/bin/env python3
# ===================================================================================
# Project: puyaisp - Programming Tool for PUYA PY32F0xx Microcontrollers
# Version: v1.4
# Year: 2023
# Author: Stefan Wagner
# Github: https://github.com/wagiminator
# License: MIT License
# ===================================================================================
#
# Description:
# ------------
# Simple Python tool for flashing PY32F0xx (and maybe other PY32) microcontrollers
# via USB-to-serial converter utilizing the factory built-in embedded boot loader.
#
# Dependencies:
# -------------
# - pyserial
#
# Operating Instructions:
# -----------------------
# You need to install PySerial to use puyaisp.
# Install it via "python3 -m pip install pyserial".
# You may need to install a driver for your USB-to-serial converter.
#
# Connect your USB-to-serial converter to your MCU:
# USB2SERIAL PY32F0xx
# RXD <--- PA2 or PA9 or PA14
# TXD ---> PA3 or PA10 or PA15
# VCC ---> VCC
# GND ---> GND
#
# Set your MCU to bootloader mode by using ONE of the following methods:
# - Disconnect your USB-to-serial converter, pull BOOT0 pin (PF4) to VCC (or press
# and hold the BOOT button, if your board has one), then connect the converter to
# your USB port. BOOT0 pin (or BOOT button) can be released now.
# - Connect your USB-to-serial converter to your USB port. Pull BOOT0 pin (PF4)
# to VCC, then pull nRST (PF2) shortly to GND (or press and hold the BOOT button,
# then press and release the RESET button and then release the BOOT button, if
# your board has them).
# These steps are not necessary when using a CH340X USB-to-serial converter with
# control lines for nRST and BOOT0. In this case, the software automatically puts
# the MCU into bootloader mode.
#
# Run "python3 puyaisp.py -f firmware.bin".
# If the PID/VID of the USB-to-Serial converter is known, it can be defined here,
# which can make the auto-detection a lot faster. If not, comment out or delete.
#PY_VID = '1A86'
#PY_PID = '7523'
# Define BAUD rate here, range: 4800 - 1000000, default and recommended: 115200
PY_BAUD = 115200
# Libraries
import sys
import time
import argparse
import serial
from serial import Serial
from serial.tools.list_ports import comports
# ===================================================================================
# Main Function
# ===================================================================================
def _main():
# Parse command line arguments
parser = argparse.ArgumentParser(description='Minimal command line interface for PY32 IAP')
parser.add_argument('-u', '--unlock', action='store_true', help='unlock chip (remove read protection)')
parser.add_argument('-l', '--lock', action='store_true', help='lock chip (set read protection)')
parser.add_argument('-e', '--erase', action='store_true', help='perform chip erase (implied with -f)')
parser.add_argument('-o', '--rstoption',action='store_true', help='reset option bytes')
parser.add_argument('-G', '--nrstgpio', action='store_true', help='make nRST pin a GPIO pin')
parser.add_argument('-R', '--nrstreset',action='store_true', help='make nRST pin a RESET pin')
parser.add_argument('-f', '--flash', help='write BIN file to flash and verify')
args = parser.parse_args(sys.argv[1:])
# Check arguments
if not any( (args.rstoption, args.unlock, args.lock, args.erase, args.nrstgpio, args.nrstreset, args.flash) ):
print('No arguments - no action!')
sys.exit(0)
# Establish connection to MCU via USB-to-serial converter
try:
print('Connecting to MCU via USB-to-serial converter ...')
isp = Programmer()
print('SUCCESS: Connection established via', isp.port + '.')
except Exception as ex:
sys.stderr.write('ERROR: ' + str(ex) + '!\n')
sys.exit(1)
# Performing actions
try:
# Get chip info
print('Getting chip info ...')
isp.readinfo()
if isp.pid == PY_CHIP_PID:
print('SUCCESS: Found PY32F0xx with bootloader v' + isp.verstr + '.')
else:
print('WARNING: Chip with PID 0x%04x is not a PY32F0xx!' % isp.pid)
# Unlock chip
if args.unlock:
print('Unlocking chip ...')
isp.unlock()
print('SUCCESS: Chip is unlocked.')
print('INFO: Other options are ignored!')
isp.reset()
print('DONE.')
sys.exit(0)
# Read option bytes and check, if chip is locked
print('Reading OPTION bytes ...')
isp.readoption()
print('SUCCESS:', isp.optionstr + '.')
# Perform chip erase
if (args.erase) or (args.flash is not None):
print('Performing chip erase ...')
isp.erase()
print('SUCCESS: Chip is erased.')
# Flash binary file
if args.flash is not None:
print('Flashing', args.flash, 'to MCU ...')
with open(args.flash, 'rb') as f: data = f.read()
isp.writeflash(PY_CODE_ADDR, data)
print('Verifying ...')
isp.verifyflash(PY_CODE_ADDR, data)
print('SUCCESS:', len(data), 'bytes written and verified.')
# Manipulate OPTION bytes (only for identified chips)
if isp.pid == PY_CHIP_PID and any( (args.rstoption, args.nrstgpio, args.nrstreset, args.lock) ):
if args.rstoption:
print('Setting OPTION bytes to default values ...')
isp.resetoption()
if args.nrstgpio:
print('Setting nRST pin as GPIO in OPTION bytes ...')
isp.nrst2gpio()
if args.nrstreset:
print('Setting nRST pin as RESET in OPTION bytes ...')
isp.nrst2reset()
if args.lock:
print('Setting read protection in OPTION BYTES ...')
isp.lock()
print('Writing OPTION bytes ...')
isp.writeoption()
print('SUCCESS: OPTION bytes written.')
isp.reset()
else:
isp.run()
except Exception as ex:
sys.stderr.write('ERROR: ' + str(ex) + '!\n')
isp.reset()
sys.exit(1)
print('DONE.')
sys.exit(0)
# ===================================================================================
# Programmer Class
# ===================================================================================
class Programmer(Serial):
def __init__(self):
# BAUD rate: 4800 - 1000000bps (default: 115200), will be auto-detected
# Data frame: 1 start bit, 8 data bit, 1 parity bit set to even, 1 stop bit
super().__init__(baudrate = PY_BAUD, parity = serial.PARITY_EVEN, timeout = 1)
self.identify()
# Identify port of programmer and enter programming mode
def identify(self):
for p in comports():
if (('PY_VID' not in globals()) or (PY_VID in p.hwid)) and (('PY_PID' not in globals()) or (PY_PID in p.hwid)):
self.port = p.device
try:
self.open()
except:
continue
self.boot()
self.reset_input_buffer()
self.write([PY_SYNCH])
if not self.checkreply():
self.close()
continue
return
raise Exception('No MCU in boot mode found')
# Send command
def sendcommand(self, command):
self.write([command, command ^ 0xff])
if not self.checkreply():
raise Exception('Device has not acknowledged the command 0x%02x' % command)
# Send address
def sendaddress(self, addr):
stream = addr.to_bytes(4, byteorder='big')
parity = 0x00
for x in range(4):
parity ^= stream[x]
self.write(stream)
self.write([parity])
if not self.checkreply():
raise Exception('Failed to send address')
# Check if device acknowledged
def checkreply(self):
reply = self.read(1)
return (len(reply) == 1 and reply[0] == PY_REPLY_ACK)
#--------------------------------------------------------------------------------
# Start bootloader
def boot(self):
self.rts = False
self.dtr = False
self.rts = True
time.sleep(0.01)
self.rts = False
time.sleep(0.01)
# Reset and disconnect
def reset(self):
self.dtr = True
self.rts = True
time.sleep(0.01)
self.rts = False
self.close()
# Start firmware and disconnect
def run(self):
self.sendcommand(PY_CMD_GO)
self.sendaddress(PY_CODE_ADDR)
self.dtr = True
self.close()
#--------------------------------------------------------------------------------
# Read info stream
def readinfostream(self, command):
self.sendcommand(command)
size = self.read(1)[0]
stream = self.read(size + 1)
if not self.checkreply():
raise Exception('Failed to read info')
return stream
# Get chip info
def readinfo(self):
self.ver = self.readinfostream(PY_CMD_GET)[0]
self.verstr = '%x.%x' % (self.ver >> 4, self.ver & 7)
self.pid = int.from_bytes(self.readinfostream(PY_CMD_PID), byteorder='big')
# Read UID
def readuid(self):
return self.readflash(PY_UID_ADDR, 128)
# Read OPTION bytes
def readoption(self):
try:
self.option = list(self.readflash(PY_OPTION_ADDR, 16))
except:
raise Exception('Chip is locked')
self.optionstr = 'OPTR: 0x%04x, SDKR: 0x%04x, WRPR: 0x%04x' % \
(( (self.option[ 0] << 8) + self.option[ 1], \
(self.option[ 4] << 8) + self.option[ 5], \
(self.option[12] << 8) + self.option[13] ))
# Write OPTION bytes
def writeoption(self):
self.writeflash(PY_OPTION_ADDR, self.option)
# Reset OPTION bytes
def resetoption(self):
self.option = list(PY_OPTION_DEFAULT)
# Set read protection in OPTION bytes
def lock(self):
self.option[0] = 0x55
self.option[2] = 0xaa
# Set nRST pin as GPIO in OPTION bytes
def nrst2gpio(self):
self.option[1] |= 0x40
self.option[3] &= 0xbf
# Set nRST pin as RESET in OPTION bytes
def nrst2reset(self):
self.option[1] &= 0xbf
self.option[3] |= 0x40
# Unlock (clear) chip and reset
def unlock(self):
self.sendcommand(PY_CMD_R_UNLOCK)
if not self.checkreply():
raise Exception('Failed to unlock chip')
#--------------------------------------------------------------------------------
# Erase whole chip
def erase(self):
self.sendcommand(PY_CMD_ERASE)
self.write(b'\xff\xff\x00')
if not self.checkreply():
raise Exception('Failed to erase chip')
# Read flash
def readflash(self, addr, size):
data = bytes()
while size > 0:
blocksize = size
if blocksize > PY_BLOCKSIZE: blocksize = PY_BLOCKSIZE
self.sendcommand(PY_CMD_READ)
self.sendaddress(addr)
self.sendcommand(blocksize - 1)
data += self.read(blocksize)
addr += blocksize
size -= blocksize
return data
# Write flash
def writeflash(self, addr, data):
size = len(data)
while size > 0:
blocksize = size
if blocksize > PY_BLOCKSIZE: blocksize = PY_BLOCKSIZE
block = data[:blocksize]
parity = blocksize - 1
for x in range(blocksize):
parity ^= block[x]
self.sendcommand(PY_CMD_WRITE)
self.sendaddress(addr)
self.write([blocksize - 1])
self.write(block)
self.write([parity])
if not self.checkreply():
raise Exception('Failed to write to address 0x%08x' % addr)
data = data[blocksize:]
addr += blocksize
size -= blocksize
# Verify flash
def verifyflash(self, addr, data):
flash = self.readflash(addr, len(data))
if set(flash) != set(data):
raise Exception('Verification failed')
# ===================================================================================
# Device Constants
# ===================================================================================
# Device and Memory constants
PY_CHIP_PID = 0x440
PY_BLOCKSIZE = 128
PY_FLASH_ADDR = 0x08000000
PY_CODE_ADDR = 0x08000000
PY_SRAM_ADDR = 0x20000000
PY_BOOT_ADDR = 0x1fff0000
PY_UID_ADDR = 0x1fff0e00
PY_OPTION_ADDR = 0x1fff0e80
PY_CONFIG_ADDR = 0x1fff0f00
# Command codes
PY_CMD_GET = 0x00
PY_CMD_VER = 0x01
PY_CMD_PID = 0x02
PY_CMD_READ = 0x11
PY_CMD_WRITE = 0x31
PY_CMD_ERASE = 0x44
PY_CMD_GO = 0x21
PY_CMD_W_LOCK = 0x63
PY_CMD_W_UNLOCK = 0x73
PY_CMD_R_LOCK = 0x82
PY_CMD_R_UNLOCK = 0x92
# Reply codes
PY_REPLY_ACK = 0x79
PY_REPLY_NACK = 0x1f
PY_REPLY_BUSY = 0xaa
# Other codes
PY_SYNCH = 0x7f
# Default option bytes
PY_OPTION_DEFAULT = b'\xaa\xbe\x55\x41\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00\x00'
# ===================================================================================
if __name__ == "__main__":
_main()