1#!/usr/bin/env python3
2#
3# Copyright (c) 2024 Nuvoton Technology Corporation
4#
5# SPDX-License-Identifier: Apache-2.0
6
7# This script will append/paste specific header to tell ROM code (Booter) of
8# NPCM eSIO series how to load the firmware from flash to code ram
9# Usage python3 ${ZEPHYR_BASE}/scripts/esiost.py
10#                    -i in_file.bin -o out_file.bin
11#                    [-chip <name>] [-v]
12
13import sys
14import hashlib
15from colorama import init, Fore
16from esiost_args import EsiostArgs, exit_with_failure
17from pathlib import Path
18
19# ESIOST
20ESIOST_VER = "1.0.0"
21
22# Offsets inside the header
23HDR_ANCHOR_OFFSET                       = 0x0
24HDR_FW_ENTRY_POINT_OFFSET               = 0x21C
25HDR_FW_FLASH_ADDR_START_LOAD_OFFSET     = 0x220
26HDR_FW_FLASH_ADDR_END_LOAD_OFFSET       = 0x224
27HDR_FW_LOAD_START_ADDR_OFFSET           = 0x228
28HDR_FW_LENGTH_OFFSET                    = 0x22C
29HDR_FW_LOAD_HASH_OFFSET                 = 0x480
30HDR_FW_SEG1_START_OFFSET                = 0x4C0
31HDR_FW_SEG1_SIZE_OFFSET                 = 0x4C4
32HDR_FW_SEG2_START_OFFSET                = 0x4C8
33HDR_FW_SEG2_SIZE_OFFSET                 = 0x4CC
34HDR_FW_SEG3_START_OFFSET                = 0x4D0
35HDR_FW_SEG3_SIZE_OFFSET                 = 0x4D4
36HDR_FW_SEG4_START_OFFSET                = 0x4D8
37HDR_FW_SEG4_SIZE_OFFSET                 = 0x4DC
38HDR_FW_SEG1_HASH_OFFSET                 = 0x500
39HDR_FW_SEG2_HASH_OFFSET                 = 0x540
40HDR_FW_SEG3_HASH_OFFSET                 = 0x580
41HDR_FW_SEG4_HASH_OFFSET                 = 0x5C0
42FW_IMAGE_OFFSET                         = 0x600
43
44ARM_FW_ENTRY_POINT_OFFSET               = 0x004
45
46# Header field known values
47FW_HDR_ANCHOR      = '%FiMg94@'
48FW_HDR_SEG1_START  = 0x210
49FW_HDR_SEG1_SIZE   = 0x2F0
50FW_HDR_SEG2_START  = 0x0
51FW_HDR_SEG2_SIZE   = 0x0
52FW_HDR_SEG3_START  = 0x600
53FW_HDR_SEG4_START  = 0x0
54FW_HDR_SEG4_SIZE   = 0x0
55
56# Header fields default values.
57ADDR_16_BYTES_ALIGNED_MASK = 0x0000000f
58ADDR_4_BYTES_ALIGNED_MASK = 0x00000003
59ADDR_4K_BYTES_ALIGNED_MASK = 0x00000fff
60
61INVALID_INPUT = -1
62HEADER_SIZE = FW_IMAGE_OFFSET
63
64# Verbose related values
65NO_VERBOSE = 0
66REG_VERBOSE = 0
67
68# Success/failure codes
69EXIT_SUCCESS_STATUS = 0
70EXIT_FAILURE_STATUS = 1
71
72def _bt_mode_handler(esiost_args):
73    """creates the bootloader table using the provided arguments.
74
75    :param esiost_args: the object representing the command line arguments.
76    """
77
78    output_file = _set_input_and_output(esiost_args)
79    _check_chip(output_file, esiost_args)
80
81    _copy_image(output_file, esiost_args)
82    _set_anchor(output_file, esiost_args)
83    _set_firmware_load_start_address(output_file, esiost_args)
84    _set_firmware_entry_point(output_file, esiost_args)
85    _set_firmware_length(output_file, esiost_args)
86    _set_firmware_load_hash(output_file, esiost_args)
87    _set_firmware_segment(output_file, esiost_args)
88    _set_firmware_segment_hash(output_file, esiost_args)
89
90    _exit_with_success()
91
92def _set_input_and_output(esiost_args):
93    """checks the input file and output and sets the output file.
94
95    checks input file existence, creates an output file according
96    to the 'output' argument.
97
98    Note: input file size has to be greater than 0, and named differently
99    from output file
100
101    :param esiost_args: the object representing the command line arguments.
102
103    :returns: output file path object, or -1 if fails
104    """
105    input_file = esiost_args.input
106    output = esiost_args.output
107    input_file_size = 0
108
109    if not input_file:
110        exit_with_failure("Define input file, using -i flag")
111
112    input_file_path = Path(input_file)
113
114    if not input_file_path.exists():
115        exit_with_failure(f'Cannot open {input_file}')
116    elif input_file_path.stat().st_size == 0:
117        exit_with_failure(f'BIN Input file ({input_file}) is empty')
118    else:
119        input_file_size = input_file_path.stat().st_size
120
121    if not output:
122        output_file = Path("out_" + input_file_path.name)
123    else:
124        output_file = Path(output)
125
126    if output_file.exists():
127        if output_file.samefile(input_file_path):
128            exit_with_failure(f'Input file name {input_file} '
129                              f'should be differed from'
130                              f' Output file name {output}')
131        output_file.unlink()
132
133    output_file.touch()
134
135    if esiost_args.verbose == REG_VERBOSE:
136        print(Fore.LIGHTCYAN_EX + f'\nBIN file: {input_file}, size:'
137              f' {input_file_size} bytes')
138        print(f'Output file name: {output_file.name} \n')
139
140    return output_file
141
142def _check_chip(output, esiost_args):
143    """checks if the chip entered is a legal chip, generates an error
144    and closes the application, deletes the output file if the chip name
145    is illegal.
146
147    :param output: the output file object,
148    :param esiost_args: the object representing the command line arguments.
149    """
150
151    if esiost_args.chip_name == INVALID_INPUT:
152        message = f'Invalid chip name, '
153        message += "should be npcm400."
154        _exit_with_failure_delete_file(output, message)
155
156def _set_anchor(output, esiost_args):
157    """writes the anchor value to the output file
158
159    :param output: the output file object.
160    :param esiost_args: the object representing the command line arguments.
161    """
162
163    if len(FW_HDR_ANCHOR) > 8:
164        message = f'ANCHOR max support 8 bytes'
165        _exit_with_failure_delete_file(output, message)
166
167    with output.open("r+b") as output_file:
168        output_file.seek(HDR_ANCHOR_OFFSET)
169        anchor_hex = FW_HDR_ANCHOR.encode('ascii')
170        output_file.write(anchor_hex)
171        if esiost_args.verbose == REG_VERBOSE:
172            print(f'- HDR - FW Header ANCHOR                 - Offset '
173                  f'{HDR_ANCHOR_OFFSET}    -  %s' % FW_HDR_ANCHOR)
174
175    output_file.close()
176
177def _set_firmware_load_start_address(output, esiost_args):
178    """writes the fw load address to the output file
179
180    :param output: the output file object,
181    :param esiost_args: the object representing the command line arguments.
182    """
183    input_file_path = Path(esiost_args.input)
184
185    start_ram = esiost_args.chip_ram_address
186    end_ram = start_ram + esiost_args.chip_ram_size
187    fw_load_addr = esiost_args.firmware_load_address
188    fw_length = esiost_args.firmware_length
189    fw_end_addr = fw_load_addr + fw_length
190    start_flash_addr = esiost_args.chip_flash_address + HEADER_SIZE
191    end_flash_addr = start_flash_addr + fw_length
192
193    start_ram_to_print = _hex_print_format(start_ram)
194    end_ram_to_print = _hex_print_format(end_ram)
195    fw_load_addr_to_print = _hex_print_format(fw_load_addr)
196    fw_end_addr_to_print = _hex_print_format(fw_end_addr)
197
198    if fw_length == INVALID_INPUT:
199        message = f'Cannot read firmware length'
200        _exit_with_failure_delete_file(output, message)
201
202    if fw_load_addr is INVALID_INPUT:
203        message = f'Cannot read FW Load start address'
204        _exit_with_failure_delete_file(output, message)
205
206    if fw_load_addr & ADDR_16_BYTES_ALIGNED_MASK != 0:
207        message = f'Firmware load address ({fw_load_addr_to_print}) ' \
208            f'is not 16 bytes aligned'
209        _exit_with_failure_delete_file(output, message)
210
211    if (fw_load_addr > end_ram) or (fw_load_addr < start_ram):
212        message = f'Firmware load address ({fw_load_addr_to_print}) ' \
213            f'should be between start ({start_ram_to_print}) '\
214            f'and end ({end_ram_to_print}) of RAM'
215        _exit_with_failure_delete_file(output, message)
216
217    with output.open("r+b") as output_file:
218        # check fw_entry pt location in flash or not
219        with input_file_path.open("r+b") as input_file:
220            input_file.seek(ARM_FW_ENTRY_POINT_OFFSET)
221            fw_arm_entry_byte = input_file.read(4)
222            fw_arm_entry_pt = int.from_bytes(fw_arm_entry_byte, "little")
223
224            if fw_arm_entry_pt == 0:
225                input_file.seek(ARM_FW_ENTRY_POINT_OFFSET + HEADER_SIZE)
226                fw_arm_entry_byte = input_file.read(4)
227                fw_arm_entry_pt = int.from_bytes(fw_arm_entry_byte, "little")
228            else:
229                if fw_end_addr > end_ram:
230                    message = f'Firmware end address ({fw_end_addr_to_print}) should be '
231                    message += f'less than end of RAM address ({end_ram_to_print})'
232                    _exit_with_failure_delete_file(output, message)
233
234            if start_flash_addr < fw_arm_entry_pt < end_flash_addr:
235                fw_load_addr = 0x0
236                start_flash_addr = 0x0
237                end_flash_addr = 0x0
238
239        input_file.close()
240
241        # set start load flash address
242        output_file.seek(HDR_FW_FLASH_ADDR_START_LOAD_OFFSET)
243        output_file.write(start_flash_addr.to_bytes(4, "little"))
244
245        # set end load flash address
246        output_file.seek(HDR_FW_FLASH_ADDR_END_LOAD_OFFSET)
247        output_file.write(end_flash_addr.to_bytes(4, "little"))
248
249        # set load start address (RAM)
250        output_file.seek(HDR_FW_LOAD_START_ADDR_OFFSET)
251        output_file.write(fw_load_addr.to_bytes(4, "little"))
252
253        if esiost_args.verbose == REG_VERBOSE:
254            print(f'- HDR - FW load start address            - Offset '
255                  f'{HDR_FW_LOAD_START_ADDR_OFFSET}  -  '
256                  f'{_hex_print_format(fw_load_addr)}')
257            print(f'- HDR - flash load start address         - Offset '
258                  f'{HDR_FW_FLASH_ADDR_START_LOAD_OFFSET}  -  '
259                  f'{_hex_print_format(start_flash_addr)}')
260            print(f'- HDR - flash load end address           - Offset '
261                  f'{HDR_FW_FLASH_ADDR_END_LOAD_OFFSET}  -  '
262                  f'{_hex_print_format(end_flash_addr)}')
263
264    output_file.close()
265
266def _set_firmware_entry_point(output, esiost_args):
267    """writes the fw entry point to the output file.
268    proportions:
269
270    :param output: the output file object,
271    :param esiost_args: the object representing the command line arguments.
272    """
273    input_file_path = Path(esiost_args.input)
274    fw_entry_pt = esiost_args.firmware_entry_point
275    start_flash_addr = esiost_args.chip_flash_address + HEADER_SIZE
276    end_flash_addr = start_flash_addr + esiost_args.chip_flash_size
277
278    # check if fwep flag wasn't set and set it to fw load address if needed
279    if fw_entry_pt is None:
280        fw_entry_pt = esiost_args.firmware_load_address
281
282    # check fw_entry pt location in flash or not
283    with input_file_path.open("r+b") as input_file:
284        input_file.seek(ARM_FW_ENTRY_POINT_OFFSET)
285        fw_arm_entry_byte = input_file.read(4)
286        fw_arm_entry_pt = int.from_bytes(fw_arm_entry_byte, "little")
287
288        if fw_arm_entry_pt == 0:
289            input_file.seek(ARM_FW_ENTRY_POINT_OFFSET + HEADER_SIZE)
290            fw_arm_entry_byte = input_file.read(4)
291            fw_arm_entry_pt = int.from_bytes(fw_arm_entry_byte, "little")
292
293        if start_flash_addr < fw_arm_entry_pt < end_flash_addr:
294            fw_entry_pt = start_flash_addr
295
296    input_file.close()
297
298    with output.open("r+b") as output_file:
299        output_file.seek(HDR_FW_ENTRY_POINT_OFFSET)
300        output_file.write(fw_entry_pt.to_bytes(4, "little"))
301    output_file.close()
302
303    if esiost_args.verbose == REG_VERBOSE:
304        print(f'- HDR - FW Entry point                   - Offset '
305              f'{HDR_FW_ENTRY_POINT_OFFSET}  -  '
306              f'{_hex_print_format(fw_entry_pt)}')
307
308def _openssl_digest(filepath):
309    """Computes the SHA-256 digest of a file using hashlib.
310
311    :param filepath: Path to the file to digest.
312    :return: The SHA-256 digest of the file as a bytearray.
313    """
314    sha256_hash = hashlib.sha256()
315    with open(filepath, "rb") as f:
316        # Read and update hash string value in blocks of 4K
317        for byte_block in iter(lambda: f.read(4096), b""):
318            sha256_hash.update(byte_block)
319    return bytearray(sha256_hash.digest())
320
321def _set_firmware_length(output, esiost_args):
322    """writes the flash size value to the output file
323    Note: the firmware length value has already been checked before
324    this method
325
326    :param output: the output file object,
327    :param esiost_args: the object representing the command line arguments.
328    """
329
330    fw_length = esiost_args.firmware_length
331    fw_length_to_print = _hex_print_format(fw_length)
332
333    with output.open("r+b") as output_file:
334        output_file.seek(HDR_FW_LENGTH_OFFSET)
335        output_file.write(fw_length.to_bytes(4, "big"))
336        if esiost_args.verbose == REG_VERBOSE:
337            print(f'- HDR - FW Length                        - Offset '
338                  f'{HDR_FW_LENGTH_OFFSET}  -  '
339                  f'{fw_length_to_print}')
340    output_file.close()
341
342def _set_firmware_load_hash(output, esiost_args):
343    """writes the load hash value to the output file
344    Note: the firmware length value has already been checked before
345    this method
346
347    :param output: the output file object,
348    :param esiost_args: the object representing the command line arguments.
349    """
350    sha256_hash = hashlib.sha256()
351    with output.open("r+b") as f:
352        f.seek(HEADER_SIZE)
353        # Read and update hash string value in blocks of 4K
354        for byte_block in iter(lambda: f.read(4096), b""):
355            sha256_hash.update(byte_block)
356
357    hash_data = bytearray(sha256_hash.digest())
358
359    with output.open("r+b") as output_file:
360        output_file.seek(HDR_FW_LOAD_HASH_OFFSET)
361        output_file.write(hash_data)
362    output_file.close()
363
364def _set_firmware_segment(output, esiost_args):
365    """writes the segment start and size value to the output file
366    Note: the firmware length value has already been checked before
367    this method
368
369    :param output: the output file object,
370    :param esiost_args: the object representing the command line arguments.
371    """
372
373    fw_length = esiost_args.firmware_length
374
375    with output.open("r+b") as output_file:
376        # set segment_1 start and size
377        output_file.seek(HDR_FW_SEG1_START_OFFSET)
378        output_file.write(FW_HDR_SEG1_START.to_bytes(4, "little"))
379        output_file.seek(HDR_FW_SEG1_SIZE_OFFSET)
380        output_file.write(FW_HDR_SEG1_SIZE.to_bytes(4, "little"))
381
382        # set segment_2 start and size
383        output_file.seek(HDR_FW_SEG2_START_OFFSET)
384        output_file.write(FW_HDR_SEG2_START.to_bytes(4, "little"))
385        output_file.seek(HDR_FW_SEG2_SIZE_OFFSET)
386        output_file.write(FW_HDR_SEG2_SIZE.to_bytes(4, "little"))
387
388        # set segment_3 start and size
389        output_file.seek(HDR_FW_SEG3_START_OFFSET)
390        output_file.write(FW_HDR_SEG3_START.to_bytes(4, "little"))
391        output_file.seek(HDR_FW_SEG3_SIZE_OFFSET)
392        output_file.write(fw_length.to_bytes(4, "little"))
393
394        # set segment_4 start and size
395        output_file.seek(HDR_FW_SEG4_START_OFFSET)
396        output_file.write(FW_HDR_SEG4_START.to_bytes(4, "little"))
397        output_file.seek(HDR_FW_SEG4_SIZE_OFFSET)
398        output_file.write(FW_HDR_SEG4_SIZE.to_bytes(4, "little"))
399
400        segment1_start_to_print = _hex_print_format(FW_HDR_SEG1_START)
401        segment1_size_to_print = _hex_print_format(FW_HDR_SEG1_SIZE)
402        segment2_start_to_print = _hex_print_format(FW_HDR_SEG2_START)
403        segment2_size_to_print = _hex_print_format(FW_HDR_SEG2_SIZE)
404        segment3_start_to_print = _hex_print_format(FW_HDR_SEG3_START)
405        segment3_size_to_print = _hex_print_format(fw_length)
406        segment4_start_to_print = _hex_print_format(FW_HDR_SEG4_START)
407        segment4_size_to_print = _hex_print_format(FW_HDR_SEG4_SIZE)
408
409        if esiost_args.verbose == REG_VERBOSE:
410            print(f'- HDR - Segment1 start address           - Offset '
411                  f'{HDR_FW_SEG1_START_OFFSET} -  '
412                  f'{segment1_start_to_print}')
413            print(f'- HDR - Segment1 size                    - Offset '
414                  f'{HDR_FW_SEG1_SIZE_OFFSET} -  '
415                  f'{segment1_size_to_print}')
416            print(f'- HDR - Segment2 start address           - Offset '
417                  f'{HDR_FW_SEG2_START_OFFSET} -  '
418                  f'{segment2_start_to_print}')
419            print(f'- HDR - Segment2 size                    - Offset '
420                  f'{HDR_FW_SEG2_SIZE_OFFSET} -  '
421                  f'{segment2_size_to_print}')
422            print(f'- HDR - Segment3 start address           - Offset '
423                  f'{HDR_FW_SEG3_START_OFFSET} -  '
424                  f'{segment3_start_to_print}')
425            print(f'- HDR - Segment3 size                    - Offset '
426                  f'{HDR_FW_SEG3_SIZE_OFFSET} -  '
427                  f'{segment3_size_to_print}')
428            print(f'- HDR - Segment4 start address           - Offset '
429                  f'{HDR_FW_SEG4_START_OFFSET} -  '
430                  f'{segment4_start_to_print}')
431            print(f'- HDR - Segment4 size                    - Offset '
432                  f'{HDR_FW_SEG4_SIZE_OFFSET} -  '
433                  f'{segment4_size_to_print}')
434
435    output_file.close()
436
437def _clearup_tempfiles(output, esiost_args):
438    """clearup the tempfiles
439
440    :param output: the output file object,
441    :param esiost_args: the object representing the command line arguments.
442    """
443
444    output_file = Path(output)
445
446    seg1_file = Path("seg1_" + output_file.name)
447    if seg1_file.exists():
448        seg1_file.unlink()
449
450def _set_firmware_segment_hash(output, esiost_args):
451    """Writes the segment hash value to the output file.
452    Note: the firmware length value has already been checked before this method.
453
454    :param output: the output file object,
455    :param esiost_args: the object representing the command line arguments.
456    """
457
458    # Generate segment files
459    with output.open("r+b") as output_file:
460        # seg1
461        output_file.seek(HDR_FW_SEG1_START_OFFSET)
462        seg1_start = int.from_bytes(output_file.read(4), "little")
463        output_file.seek(HDR_FW_SEG1_SIZE_OFFSET)
464        seg1_size = int.from_bytes(output_file.read(4), "little")
465        output_file.seek(seg1_start)
466
467        seg1_data = output_file.read(seg1_size)
468
469        seg1_file_path = Path("seg1_" + output_file.name)
470        with seg1_file_path.open("wb") as seg1_file:
471            seg1_file.write(seg1_data)
472
473        # set hash
474
475        # seg1 hash
476        hash_data = _openssl_digest(seg1_file_path)
477        output_file.seek(HDR_FW_SEG1_HASH_OFFSET)
478        output_file.write(hash_data)
479
480        # seg3 hash
481        sha256_hash = hashlib.sha256()
482        output_file.seek(HEADER_SIZE)
483        # Read and update hash string value in blocks of 4K
484        for byte_block in iter(lambda: output_file.read(4096), b""):
485            sha256_hash.update(byte_block)
486
487        hash_data = bytearray(sha256_hash.digest())
488
489        output_file.seek(HDR_FW_SEG3_HASH_OFFSET)
490        output_file.write(hash_data)
491
492    _clearup_tempfiles(output, esiost_args)
493
494def _copy_image(output, esiost_args):
495    """copies the fw image from the input file to the output file
496    if firmware header offset is defined, just copies the input file to the
497    output file
498
499    :param output: the output file object,
500    :param esiost_args: the object representing the command line arguments.
501    """
502
503    # check input file offset
504    with open(esiost_args.input, "rb") as firmware_image:
505        firmware_image.seek(ARM_FW_ENTRY_POINT_OFFSET)
506        fw_arm_entry_byte = firmware_image.read(4)
507        fw_arm_entry_pt = int.from_bytes(fw_arm_entry_byte, "little")
508        if fw_arm_entry_pt != 0:
509            image_offset = 0
510            input_file_size = Path(esiost_args.input).stat().st_size
511        else:
512            firmware_image.seek(ARM_FW_ENTRY_POINT_OFFSET + HEADER_SIZE)
513            fw_arm_entry_byte = firmware_image.read(4)
514            fw_arm_entry_pt = int.from_bytes(fw_arm_entry_byte, "little")
515            if fw_arm_entry_pt == 0:
516                sys.exit(EXIT_FAILURE_STATUS)
517            else:
518                image_offset = 1
519                input_file_size = Path(esiost_args.input).stat().st_size - HEADER_SIZE
520
521    firmware_image.close()
522
523    with open(esiost_args.input, "rb") as firmware_image:
524        with open(output, "r+b") as output_file:
525            if image_offset == 0:
526                output_file.seek(HEADER_SIZE)
527            for line in firmware_image:
528                output_file.write(line)
529        output_file.close()
530    firmware_image.close()
531
532    # update firmware length if needed
533    fw_length = esiost_args.firmware_length
534    if fw_length is None:
535        esiost_args.firmware_length = input_file_size
536
537def _hex_print_format(value):
538    """hex representation of an integer
539
540    :param value: an integer to be represented in hex
541    """
542    return "0x{:08x}".format(value)
543
544def _exit_with_failure_delete_file(output, message):
545    """formatted failure message printer, prints the
546    relevant error message, deletes the output file,
547    and exits the application.
548
549    :param message: the error message to be printed
550    """
551    output_file = Path(output)
552    if output_file.exists():
553        output_file.unlink()
554
555    message = '\n' + message
556    message += '\n'
557    message += '******************************\n'
558    message += '***        FAILED          ***\n'
559    message += '******************************\n'
560    print(Fore.RED + message)
561
562    sys.exit(EXIT_FAILURE_STATUS)
563
564def _exit_with_success():
565    """formatted success message printer, prints the
566    success message and exits the application.
567    """
568    message = '\n'
569    message += '******************************\n'
570    message += '***        SUCCESS         ***\n'
571    message += '******************************\n'
572    print(Fore.GREEN + message)
573
574    sys.exit(EXIT_SUCCESS_STATUS)
575
576def main():
577    """main of the application
578    """
579    init()  # colored print initialization for windows
580
581    if len(sys.argv) < 2:
582        sys.exit(EXIT_FAILURE_STATUS)
583
584    esiost_obj = EsiostArgs()
585
586    if esiost_obj.error_args:
587        for err_arg in esiost_obj.error_args:
588            message = f'unKnown flag: {err_arg}'
589            exit_with_failure(message)
590        sys.exit(EXIT_SUCCESS_STATUS)
591
592    # Start to handle booter header table
593    _bt_mode_handler(esiost_obj)
594
595if __name__ == '__main__':
596    main()
597