1+ #!/usr/bin/env python
2+ # SparkFun Variable Loader
3+ # Variable baud rate bootloader for Artemis Apollo3 modules
4+
5+ # Immediately upon reset the Artemis module will search for the timing character
6+ # to auto-detect the baud rate. If a valid baud rate is found the Artemis will
7+ # respond with the bootloader version packet
8+ # If the computer receives a well-formatted version number packet at the desired
9+ # baud rate it will send a command to begin bootloading. The Artemis shall then
10+ # respond with the a command asking for the next frame.
11+ # The host will then send a frame packet. If the CRC is OK the Artemis will write
12+ # that to memory and request the next frame. If the CRC fails the Artemis will
13+ # discard that data and send a request to re-send the previous frame.
14+ # This cycle repeats until the Artemis receives a done command in place of the
15+ # requested frame data command.
16+ # The initial baud rate determination must occur within some small timeout. Once
17+ # baud rate detection has completed all additional communication will have a
18+ # universal timeout value. Once the Artemis has begun requesting data it may no
19+ # no longer exit the bootloader. If the host detects a timeout at any point it
20+ # will stop bootloading.
21+
22+ # Notes about PySerial timeout:
23+ # The timeout operates on whole functions - that is to say that a call to
24+ # ser.read(10) will return after ser.timeout, just as will ser.read(1) (assuming
25+ # that the necessary bytes were not found)
26+ # If there are no incoming bytes (on the line or in the buffer) then two calls to
27+ # ser.read(n) will time out after 2*ser.timeout
28+ # Incoming UART data is buffered behind the scenes, probably by the OS.
29+
30+ # ***********************************************************************************
31+ #
32+ # Imports
33+ #
34+ # ***********************************************************************************
35+
36+ import argparse
37+ import serial
38+ import serial .tools .list_ports as list_ports
39+ import sys
40+ import time
41+ import math
42+
43+ # ***********************************************************************************
44+ #
45+ # Commands
46+ #
47+ # ***********************************************************************************
48+ SVL_CMD_VER = 0x01 # version
49+ SVL_CMD_BL = 0x02 # enter bootload mode
50+ SVL_CMD_NEXT = 0x03 # request next chunk
51+ SVL_CMD_FRAME = 0x04 # indicate app data frame
52+ SVL_CMD_RETRY = 0x05 # request re-send frame
53+ SVL_CMD_DONE = 0x06 # finished - all data sent
54+
55+
56+ # ***********************************************************************************
57+ #
58+ # Compute CRC on a byte array
59+ #
60+ # ***********************************************************************************
61+ def get_crc16 (data ):
62+ # To perform the division perform the following:
63+
64+ # Load the register with zero bits.
65+ # Augment the message by appending W zero bits to the end of it.
66+ # While (more message bits)
67+ # Begin
68+ # Shift the register left by one bit, reading the next bit of the
69+ # augmented message into register bit position 0.
70+ # If (a 1 bit popped out of the register during step 3)
71+ # Register = Register XOR Poly.
72+ # End
73+ # The register now contains the remainder.
74+ register = 0x0000
75+ poly = 0x8005
76+
77+ data = bytearray (data )
78+ data .extend (bytearray (2 ))
79+ bits = 8 * len (data )
80+
81+ def get_data_bit (bit ):
82+ byte = int (bit / 8 )
83+ if (data [byte ] & (0x80 >> (bit % 8 ))):
84+ return 1
85+ return 0
86+
87+ for bit in range (bits ):
88+
89+ c = 0
90+ if (register & 0x8000 ):
91+ c = 1
92+
93+ register <<= 1
94+ register &= 0xFFFF
95+
96+ if (get_data_bit (bit )):
97+ register |= 0x0001
98+
99+ if (c ):
100+ register = (register ^ poly )
101+
102+ return register
103+
104+
105+
106+ # ***********************************************************************************
107+ #
108+ # Wait for a packet
109+ #
110+ # ***********************************************************************************
111+ def wait_for_packet (ser ):
112+
113+ packet = {'len' :0 , 'cmd' :0 , 'data' :0 , 'crc' :1 , 'timeout' :1 }
114+
115+ n = ser .read (2 ) # get the number of bytes
116+ if (len (n ) < 2 ):
117+ return packet
118+
119+ packet ['len' ] = int .from_bytes (n , byteorder = 'big' , signed = False ) #
120+ payload = ser .read (packet ['len' ])
121+
122+ if (len (payload ) != packet ['len' ]):
123+ return packet
124+
125+ packet ['timeout' ] = 0 # all bytes received, so timeout is not true
126+ packet ['cmd' ] = payload [0 ] # cmd is the first byte of the payload
127+ packet ['data' ] = payload [1 :packet ['len' ]- 2 ] # the data is the part of the payload that is not cmd or crc
128+ packet ['crc' ] = get_crc16 (payload ) # performing the crc on the whole payload should return 0
129+
130+ return packet
131+
132+ # ***********************************************************************************
133+ #
134+ # Send a packet
135+ #
136+ # ***********************************************************************************
137+ def send_packet (ser , cmd , data ):
138+ data = bytearray (data )
139+ num_bytes = 3 + len (data )
140+ payload = bytearray (cmd .to_bytes (1 ,'big' ))
141+ payload .extend (data )
142+ crc = get_crc16 (payload )
143+ payload .extend (bytearray (crc .to_bytes (2 ,'big' )))
144+
145+ ser .write (num_bytes .to_bytes (2 ,'big' ))
146+ ser .write (bytes (payload ))
147+
148+
149+
150+
151+
152+
153+ # ***********************************************************************************
154+ #
155+ # Setup: signal baud rate, get version, and command BL enter
156+ #
157+ # ***********************************************************************************
158+ def phase_setup (ser ):
159+
160+ baud_detect_byte = b'U'
161+
162+ print ('\n phase:\t setup' )
163+
164+ # Handle the serial startup blip
165+ ser .reset_input_buffer ()
166+ print ('\t cleared startup blip' )
167+
168+ ser .write (baud_detect_byte ) # send the baud detection character
169+
170+ packet = wait_for_packet (ser )
171+ if (packet ['timeout' ] or packet ['crc' ]):
172+ return 1
173+
174+ print ('\t Got SVL Bootloader Version: ' + str (int .from_bytes (packet ['data' ],'big' )))
175+ print ('\t Sending \' enter bootloader\' command' )
176+
177+ send_packet (ser , SVL_CMD_BL , b'' )
178+
179+ # Now enter the bootload phase
180+
181+
182+
183+
184+
185+
186+ # ***********************************************************************************
187+ #
188+ # Bootloader phase (Artemis is locked in)
189+ #
190+ # ***********************************************************************************
191+ def phase_bootload (ser ):
192+
193+ frame_size = 512 * 4
194+
195+ print ('\n phase:\t bootload' )
196+
197+ with open (args .binfile , mode = 'rb' ) as binfile :
198+ application = binfile .read ()
199+ total_len = len (application )
200+
201+ total_frames = math .ceil (total_len / frame_size )
202+ curr_frame = 0
203+
204+ verboseprint ('\t Length to send: ' + str (total_len ) + ' bytes, ' + str (total_frames ) + ' frames' )
205+
206+ bl_done = False
207+ while (not bl_done ):
208+
209+ packet = wait_for_packet (ser ) # wait for indication by Artemis
210+ if (packet ['timeout' ] or packet ['crc' ]):
211+ print ('\n \t error receiving packet' )
212+ print (packet )
213+ print ('\n ' )
214+ return 1
215+
216+ if ( packet ['cmd' ] == SVL_CMD_NEXT ):
217+ # verboseprint('\tgot frame request')
218+ curr_frame += 1
219+ elif ( packet ['cmd' ] == SVL_CMD_RETRY ):
220+ verboseprint ('\t \t retrying...' )
221+ else :
222+ print ('unknown error' )
223+ return 1
224+
225+ if ( curr_frame <= total_frames ):
226+ frame_data = application [((curr_frame - 1 )* frame_size ):((curr_frame - 1 + 1 )* frame_size )]
227+ verboseprint ('\t sending frame #' + str (curr_frame )+ ', length: ' + str (len (frame_data )))
228+
229+ send_packet (ser , SVL_CMD_FRAME , frame_data )
230+
231+ else :
232+ send_packet (ser , SVL_CMD_DONE , b'' )
233+ bl_done = True
234+
235+ print ('\n \t Upload complete' )
236+
237+ exit ()
238+
239+
240+
241+
242+
243+
244+ # ***********************************************************************************
245+ #
246+ # Help if serial port could not be opened
247+ #
248+ # ***********************************************************************************
249+ def phase_serial_port_help ():
250+ devices = list_ports .comports ()
251+
252+ # First check to see if user has the given port open
253+ for dev in devices :
254+ if (dev .device .upper () == args .port .upper ()):
255+ print (dev .device + " is currently open. Please close any other terminal programs that may be using " +
256+ dev .device + " and try again." )
257+ exit ()
258+
259+ # otherwise, give user a list of possible com ports
260+ print (args .port .upper () +
261+ " not found but we detected the following serial ports:" )
262+ for dev in devices :
263+ if 'CH340' in dev .description :
264+ print (
265+ dev .description + ": Likely an Arduino or derivative. Try " + dev .device + "." )
266+ elif 'FTDI' in dev .description :
267+ print (
268+ dev .description + ": Likely an Arduino or derivative. Try " + dev .device + "." )
269+ elif 'USB Serial Device' in dev .description :
270+ print (
271+ dev .description + ": Possibly an Arduino or derivative." )
272+ else :
273+ print (dev .description )
274+
275+
276+ # ***********************************************************************************
277+ #
278+ # Main function
279+ #
280+ # ***********************************************************************************
281+ def main ():
282+ try :
283+ with serial .Serial (args .port , args .baud , timeout = args .timeout ) as ser :
284+
285+ t_su = 0.15 # startup time for Artemis bootloader (experimentally determined - 0.095 sec min delay)
286+
287+ print ('\n \n Artemis SVL Bootloader' )
288+
289+ time .sleep (t_su ) # Allow Artemis to come out of reset
290+ phase_setup (ser ) # Perform baud rate negotiation
291+
292+ phase_bootload (ser ) # Bootload
293+
294+ except :
295+ phase_serial_port_help ()
296+
297+ exit ()
298+
299+
300+ # ******************************************************************************
301+ #
302+ # Main program flow
303+ #
304+ # ******************************************************************************
305+ if __name__ == '__main__' :
306+
307+ parser = argparse .ArgumentParser (
308+ description = 'SparkFun Serial Bootloader for Artemis' )
309+
310+ parser .add_argument ('port' , help = 'Serial COMx Port' )
311+
312+ parser .add_argument ('-b' , dest = 'baud' , default = 115200 , type = int ,
313+ help = 'Baud Rate (default is 115200)' )
314+
315+ parser .add_argument ('-f' , dest = 'binfile' , default = '' ,
316+ help = 'Binary file to program into the target device' )
317+
318+ parser .add_argument ("-v" , "--verbose" , default = 0 , help = "Enable verbose output" ,
319+ action = "store_true" )
320+
321+ parser .add_argument ("-t" , "--timeout" , default = 0.50 , help = "Communication timeout in seconds (default 0.5)" ,
322+ type = float )
323+
324+ if len (sys .argv ) < 2 :
325+ print ("No port selected. Detected Serial Ports:" )
326+ devices = list_ports .comports ()
327+ for dev in devices :
328+ print (dev .description )
329+
330+ args = parser .parse_args ()
331+
332+ # Create print function for verbose output if caller deems it: https://stackoverflow.com/questions/5980042/how-to-implement-the-verbose-or-v-option-into-a-script
333+ if args .verbose :
334+ def verboseprint (* args ):
335+ # Print each argument separately so caller doesn't need to
336+ # stuff everything to be printed into a single string
337+ for arg in args :
338+ print (arg , end = '' ),
339+ print ()
340+ else :
341+ verboseprint = lambda * a : None # do-nothing function
342+
343+ main ()
0 commit comments