diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bae1e5888..3dcc5f068 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -377,6 +377,13 @@ target_esp32s3_jtag_serial: script: - coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_esptool.py --port /dev/serial_ports/ESP32S3_JTAG_SERIAL --preload-port /dev/serial_ports/ESP32S3_PRELOAD --chip esp32s3 --baud 115200 +target_esp32s3_sdm: + extends: .target_esptool_test + tags: + - esptool_esp32s3_sdm_target + script: + - coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_esptool_sdm.py --port /dev/serial_ports/ESP32S3_SDM --chip esp32s3 --baud 115200 + # ESP32C2 target_esp32c2_40mhz: extends: .target_esptool_test diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 11ec9aec4..de0ca91ed 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -151,6 +151,8 @@ The following tests are not run automatically by GitHub Actions, because they ne Some tests might fail at higher baud rates on some hardware. +* ``test_esptool_sdm.py`` contains integration tests for ``esptool.py`` with chips in secure download mode. It needs to be run against real Espressif hardware (with active SDM). The command line format is the same as for ``test_esptool.py``. + The following tests are not run automatically by GitHub Actions, but can be run locally in a command line: * ``test_espefuse.py`` tests ``espefuse.py`` functionality. To run it: diff --git a/espefuse/efuse/base_fields.py b/espefuse/efuse/base_fields.py index 5dedd9f2b..f0bbbe02f 100644 --- a/espefuse/efuse/base_fields.py +++ b/espefuse/efuse/base_fields.py @@ -228,13 +228,14 @@ def get_offsets(self): return [self.parent.read_reg(offs) for offs in get_offsets(self)] - def read(self): + def read(self, print_info=True): words = self.get_words() data = BitArray() for word in reversed(words): data.append("uint:32=%d" % word) self.bitarray.overwrite(data, pos=0) - self.print_block(self.bitarray, "read_regs") + if print_info: + self.print_block(self.bitarray, "read_regs") def print_block(self, bit_string, comment, debug=False): if self.parent.debug or debug: @@ -386,6 +387,18 @@ def burn_words(self, words): ) break if not self.fail and self.num_errors == 0: + self.read(print_info=False) + if self.wr_bitarray & self.bitarray != self.wr_bitarray: + # if the required bits are not set then we need to re-burn it again. + if burns < 2: + print( + f"\nRepeat burning BLOCK{self.id} (#{burns + 2}) because not all bits were set" + ) + continue + else: + print( + f"\nAfter {burns + 1} attempts, the required data was not set to BLOCK{self.id}" + ) break def burn(self): diff --git a/test/test_esptool.py b/test/test_esptool.py index 5f7975760..438748bb6 100755 --- a/test/test_esptool.py +++ b/test/test_esptool.py @@ -204,7 +204,7 @@ def run_esptool_process(cmd): print(output) # for more complete stdout logs on failure return output - def run_esptool_error(self, args, baud=None): + def run_esptool_error(self, args, baud=None, chip=None): """ Run esptool.py similar to run_esptool, but expect an error. @@ -212,9 +212,9 @@ def run_esptool_error(self, args, baud=None): and returns the output from esptool.py as a string. """ with pytest.raises(subprocess.CalledProcessError) as fail: - self.run_esptool(args, baud) + self.run_esptool(args, baud, chip) failure = fail.value - assert failure.returncode == 2 # esptool.FatalError return code + assert failure.returncode in [1, 2] # UnsupportedCmdError and FatalError codes return failure.output.decode("utf-8") @classmethod diff --git a/test/test_esptool_sdm.py b/test/test_esptool_sdm.py new file mode 100644 index 000000000..eacf107d5 --- /dev/null +++ b/test/test_esptool_sdm.py @@ -0,0 +1,69 @@ +# Unit tests (really integration tests) for esptool.py using the pytest framework +# Uses a device in the Secure Download Mode connected to the serial port. +# +# RUNNING THIS WILL MESS UP THE DEVICE'S SPI FLASH CONTENTS +# +# How to use: +# +# Run with a physical connection to a chip: +# - `pytest test_esptool_sdm.py --chip esp32 --port /dev/ttyUSB0 --baud 115200` +# +# where - --port - a serial port for esptool.py operation +# - --chip - ESP chip name +# - --baud - baud rate +# - --with-trace - trace all interactions (True or False) + +from test_esptool import EsptoolTestCase, arg_chip, esptool, pytest + + +@pytest.mark.skipif( + arg_chip == "esp8266", reason="ESP8266 does not support Secure Download Mode" +) +class TestSecureDownloadMode(EsptoolTestCase): + expected_chip_name = esptool.util.expand_chip_name(arg_chip) + + def test_auto_detect(self): + output = self.run_esptool_error("flash_id", chip="auto") + + if arg_chip in ["esp32", "esp32s2"]: # no autodetection with get_security_info + assert "Secure Download Mode is enabled" in output + assert "Unsupported detection protocol" in output + else: + assert "Unsupported detection protocol" not in output + assert f"Detecting chip type... {self.expected_chip_name}" in output + assert "Stub loader is not supported in Secure Download Mode" in output + assert ( + f"Chip is {self.expected_chip_name} in Secure Download Mode" in output + ) + + # Commands not supported in SDM + def test_sdm_incompatible_commands(self): + output = self.run_esptool_error("flash_id") # flash_id + assert "This command (0xa) is not supported in Secure Download Mode" in output + + output = self.run_esptool_error("read_flash 0 10 out.bin") # read_flash + assert "This command (0xe) is not supported in Secure Download Mode" in output + + output = self.run_esptool_error("erase_flash") # erase_flash + assert ( + f"{self.expected_chip_name} ROM does not support function erase_flash" + in output + ) + + # Commands supported in SDM + def test_sdm_compatible_commands(self): + output = self.run_esptool("write_flash 0x0 images/one_kb.bin") # write_flash + assert "Security features enabled, so not changing any flash settings" in output + assert "Wrote 1024 bytes" in output + assert "Hash of data verified." not in output # Verification not supported + + output = self.run_esptool_error( + "write_flash --flash_size detect 0x0 images/one_kb.bin" + ) + assert ( + "Detecting flash size is not supported in secure download mode." in output + ) + + if arg_chip != "esp32": # esp32 does not support get_security_info + output = self.run_esptool("get_security_info") # get_security_info + assert "Security Information:" in output