diff --git a/docs/esp32/img/esp32.jpg b/docs/esp32/img/esp32.jpg index a96ddcbe6a..3d361bf07b 100644 Binary files a/docs/esp32/img/esp32.jpg and b/docs/esp32/img/esp32.jpg differ diff --git a/docs/reference/pyboard.py.rst b/docs/reference/pyboard.py.rst index a06ffdcd8f..d1d6818ece 100644 --- a/docs/reference/pyboard.py.rst +++ b/docs/reference/pyboard.py.rst @@ -25,7 +25,11 @@ Running ``pyboard.py --help`` gives the following output: .. code-block:: text usage: pyboard [-h] [-d DEVICE] [-b BAUDRATE] [-u USER] [-p PASSWORD] - [-c COMMAND] [-w WAIT] [--follow | --no-follow] [-f] + [-c COMMAND] [-w WAIT] [--follow | --no-follow] + + + +[-f] [files [files ...]] Run scripts on the pyboard. @@ -48,6 +52,10 @@ Running ``pyboard.py --help`` gives the following output: available --follow follow the output after running the scripts [default if no scripts given] + --no-soft-reset Prevent performing a soft reset when connecting to MCU + --hard-reset pulse the MCU reset pin to hard-reset the MCU (if your + serial connection wires RTS to reset pin properly) + --no-exclusive Open the serial device shared (not exclusive) access -f, --filesystem perform a filesystem action: cp local :device | cp :device local | cat path | ls [path] | rm path | mkdir path | rmdir path diff --git a/ports/esp32/boards/ESP32_CAM/board.json b/ports/esp32/boards/ESP32_CAM/board.json new file mode 100644 index 0000000000..771bddc2fd --- /dev/null +++ b/ports/esp32/boards/ESP32_CAM/board.json @@ -0,0 +1,25 @@ +{ + "deploy": [ + "../deploy.md" + ], + "docs": "", + "features": [ + "External Flash", + "WiFi" + ], + "images": [ + "esp32_cam.jpg" + ], + "mcu": "esp32", + "product": "ESP32 / CAM", + "thumbnail": "", + "url": "https://www.espressif.com/en/products/modules", + "variants": { + "IDF3": "Compiled with IDF 3.x", + "D2WD": "ESP32 D2WD", + "SPIRAM": "Support for SPIRAM / WROVER", + "UNICORE": "ESP32 Unicore", + "OTA": "Support for OTA" + }, + "vendor": "Espressif" +} diff --git a/ports/esp32/boards/ESP32_CAM/board.md b/ports/esp32/boards/ESP32_CAM/board.md new file mode 100644 index 0000000000..431c990e33 --- /dev/null +++ b/ports/esp32/boards/ESP32_CAM/board.md @@ -0,0 +1,29 @@ +The following files are firmware that work on the ESP32 CAM module using +an OV2640 camera. + +ESP32 CAM has PSRAM (aka spiram) but the ESP32-S chip on it does not support bluetooth + +To build this from source, load your env vars:- + + . $HOME/esp/esp-idf/export.sh + +Open a shell in the folder micropython/ports/esp32 + +and run this command:- + + git clone https://github.com/lemariva/micropython-camera-driver.git + git clone https://github.com/espressif/esp32-camera + make BOARD=ESP32_CAM submodules + make USER_C_MODULES=../micropython-camera-driver/src/micropython.cmake BOARD=ESP32_CAM all + +-or- (for ota support):- + + make USER_C_MODULES=../micropython-camera-driver/src/micropython.cmake BOARD=ESP32_CAM MICROPY_BOARD_VARIANT=OTA + +then flash is like this:- + + esptool.py -p $PORT write_flash --flash_mode dio --flash_size 4MB --flash_freq 40m 0x1000 build-ESP32_CAM/bootloader/bootloader.bin 0x8000 build-ESP32_CAM/partition_table/partition-table.bin 0x10000 build-ESP32_CAM/micropython.bin + +Note that these boards wire RTS and DSR to reset and gpio0 pins, so you need the fixed pyboard.py that includes the --hard-reset to talk to them. For example:- + + pyboard.py --device $PORT --hard-reset -f ls diff --git a/ports/esp32/boards/ESP32_CAM/mpconfigboard.cmake b/ports/esp32/boards/ESP32_CAM/mpconfigboard.cmake new file mode 100644 index 0000000000..5ff5fabe99 --- /dev/null +++ b/ports/esp32/boards/ESP32_CAM/mpconfigboard.cmake @@ -0,0 +1,44 @@ +set(SDKCONFIG_DEFAULTS + boards/sdkconfig.base + boards/sdkconfig.spiram +) + +# boards/sdkconfig.ble +# boards/ESP32_CAM/sdkconfig.esp32cam + +list(APPEND MICROPY_DEF_BOARD + MICROPY_HW_MCU_NAME="ESP32" + # Disable some options to reduce firmware size. + # MICROPY_OPT_COMPUTED_GOTO=0 + # MICROPY_PY_NETWORK_LAN=0 + # ESP32-CAMERA + CONFIG_OV2640_SUPPORT=y + MICROPY_HW_BOARD_NAME="ESP32S CAM module with SPIRAM and OV2640" + CONFIG_COMPILER_OPTIMIZATION_SIZE=n + CONFIG_COMPILER_OPTIMIZATION_PERF=y +) + + +if(MICROPY_BOARD_VARIANT STREQUAL "OTA") + set(SDKCONFIG_DEFAULTS + ${SDKCONFIG_DEFAULTS} + boards/ESP32_GENERIC/sdkconfig.ota + ) + + list(APPEND MICROPY_DEF_BOARD + MICROPY_HW_BOARD_NAME="Generic ESP32 module with OTA" + ) +endif() + + + +if(MICROPY_BOARD_VARIANT STREQUAL "UNICORE") + set(SDKCONFIG_DEFAULTS + ${SDKCONFIG_DEFAULTS} + boards/ESP32_GENERIC/sdkconfig.unicore + ) + + list(APPEND MICROPY_DEF_BOARD + MICROPY_HW_MCU_NAME="ESP32-UNICORE" + ) +endif() diff --git a/ports/esp32/boards/ESP32_CAM/mpconfigboard.h b/ports/esp32/boards/ESP32_CAM/mpconfigboard.h new file mode 100644 index 0000000000..22cf1fcdc2 --- /dev/null +++ b/ports/esp32/boards/ESP32_CAM/mpconfigboard.h @@ -0,0 +1,13 @@ +// Both of these can be set by mpconfigboard.cmake if a BOARD_VARIANT is +// specified. + +#ifndef MICROPY_HW_BOARD_NAME +#define MICROPY_HW_BOARD_NAME "ESP32S CAM module with PSRAM and OV2640" +#endif + +#define MICROPY_PY_BLUETOOTH (0) +#define MODULE_CAMERA_ENABLED (1) + +#ifndef MICROPY_HW_MCU_NAME +#define MICROPY_HW_MCU_NAME "ESP32" +#endif diff --git a/ports/esp32/boards/ESP32_CAM/photo.py b/ports/esp32/boards/ESP32_CAM/photo.py new file mode 100644 index 0000000000..d33f82aba0 --- /dev/null +++ b/ports/esp32/boards/ESP32_CAM/photo.py @@ -0,0 +1,171 @@ +# photo.py + +__version__ = "1.0.3" # Major.Minor.Patch + +# Ways to run this program:- + +# 1. With ampy +# ampy --port $PORT run bin/photo.py +# example output:- +# photo fn=out.jpg size=22((default)) quality=10 +# Length of buf: 23579 + +# 2. From REPL shell +# >>> ARGV=["pic.jpg","5","10"];exec(open("bin/photo.py").read()) +# example output:- +# photo fn=pic.jpg size=5(FRAME_QVGA) quality=10 +# Length of buf: 9495 + +# 3. using mipyshell +# To run this program with arguments, install https://github.com/vsolina/mipyshell +# and save this file as bin/photo.py - then (for size 5 and quality 10):- +# photo outfile.jpg 5 10 + + +import camera + + +def __main__(args): + capture(args[2:]) # mipyshell first 2 arguments are "python" and "photo.py" + + +def capture(args): + fn = "out.jpg" + quality = 10 + size = 22 + + camera_frames = { + 0: {"name": "FRAME_96X96", "value": camera.FRAME_96X96}, + 1: {"name": "FRAME_QQVGA", "value": camera.FRAME_QQVGA}, + 2: {"name": "FRAME_QCIF", "value": camera.FRAME_QCIF}, + 3: {"name": "FRAME_HQVGA", "value": camera.FRAME_HQVGA}, + 4: {"name": "FRAME_240X240", "value": camera.FRAME_240X240}, + 5: {"name": "FRAME_QVGA", "value": camera.FRAME_QVGA}, + 6: {"name": "FRAME_CIF", "value": camera.FRAME_CIF}, + 7: {"name": "FRAME_HVGA", "value": camera.FRAME_HVGA}, + 8: {"name": "FRAME_VGA", "value": camera.FRAME_VGA}, + 9: {"name": "FRAME_SVGA", "value": camera.FRAME_SVGA}, + 10: {"name": "FRAME_XGA", "value": camera.FRAME_XGA}, + 11: {"name": "FRAME_HD", "value": camera.FRAME_HD}, + 12: {"name": "FRAME_SXGA", "value": camera.FRAME_SXGA}, + 13: {"name": "FRAME_UXGA", "value": camera.FRAME_UXGA}, + 14: {"name": "FRAME_FHD", "value": camera.FRAME_FHD}, + 15: {"name": "FRAME_P_HD", "value": camera.FRAME_P_HD}, + 16: {"name": "FRAME_P_3MP", "value": camera.FRAME_P_3MP}, + 17: {"name": "FRAME_QXGA", "value": camera.FRAME_QXGA}, + 18: {"name": "FRAME_QHD", "value": camera.FRAME_QHD}, + 19: {"name": "FRAME_WQXGA", "value": camera.FRAME_WQXGA}, + 20: {"name": "FRAME_P_FHD", "value": camera.FRAME_P_FHD}, + 21: {"name": "FRAME_QSXGA", "value": camera.FRAME_QSXGA}, + 22: {"name": "(default)", "value": None}, + } + + if len(args) > 0: + fn = args[0] + + ## ESP32-CAM (default configuration) - https://bit.ly/2Ndn8tN + camera.init(0, format=camera.JPEG, fb_location=camera.PSRAM) + + if len(args) > 1: + size = int(args[1]) + camera.framesize(camera_frames[size]["value"]) + + if len(args) > 2: + quality = int(args[2]) + camera.quality(quality) + + print( + "photo fn={} size={}({}) quality={}".format(fn, size, camera_frames[size]["name"], quality) + ) + + # AI-Thinker esp32-cam board + # ai_thinker = {PIN_PWDN:32, PIN_RESET:-1, PIN_XCLK:0, PIN_SIOD:26, PIN_SIOC:27, PIN_D7:35, PIN_D6:34, PIN_D5:39, PIN_D4:36, PIN_D3:21, PIN_D2:19, PIN_D1:18, PIN_D0:5, PIN_VSYNC:25, PIN_HREF:23, PIN_PCLK:22, XCLK_MHZ:16, PIXFORMAT:5, FRAMESIZE:10, JPEG_QUALITY:10, FB_COUNT:1, } + + ## M5Camera (Version B) - https://bit.ly/317Xb74 + # camera.init(0, d0=32, d1=35, d2=34, d3=5, d4=39, d5=18, d6=36, d7=19, format=camera.JPEG, framesize=camera.FRAME_VGA, xclk_freq=camera.XCLK_10MHz, href=26, vsync=25, reset=15, sioc=23, siod=22, xclk=27, pclk=21, fb_location=camera.PSRAM) #M5CAMERA + + ## T-Camera Mini (green PCB) - https://bit.ly/31H1aaF + # import axp202 # source https://github.com/lewisxhe/AXP202_PythonLibrary + # USB current limit must be disabled (otherwise init fails) + # axp=axp202.PMU( scl=22, sda=21, address=axp202.AXP192_SLAVE_ADDRESS ) + # limiting=axp.read_byte( axp202.AXP202_IPS_SET ) + # limiting &= 0xfc + # axp.write_byte( axp202.AXP202_IPS_SET, limiting ) + + # camera.init(0, d0=5, d1=14, d2=4, d3=15, d4=18, d5=23, d6=36, d7=39, format=camera.JPEG, framesize=camera.FRAME_VGA, xclk_freq=camera.XCLK_20MHz, href=25, vsync=27, reset=-1, pwdn=-1, sioc=12, siod=13, xclk=32, pclk=19) + + # The parameters: format=camera.JPEG, xclk_freq=camera.XCLK_10MHz are standard for all cameras. + # You can try using a faster xclk (20MHz), this also worked with the esp32-cam and m5camera + # but the image was pixelated and somehow green. + + # ## Other settings: + # # flip up side down + # camera.flip(1) + # # left / right + # camera.mirror(1) + + # # framesize + # camera.framesize(camera.FRAME_240x240) + # # The options are the following: + # # FRAME_96X96 FRAME_QQVGA FRAME_QCIF FRAME_HQVGA FRAME_240X240 + # # FRAME_QVGA FRAME_CIF FRAME_HVGA FRAME_VGA FRAME_SVGA + # # FRAME_XGA FRAME_HD FRAME_SXGA FRAME_UXGA FRAME_FHD + # # FRAME_P_HD FRAME_P_3MP FRAME_QXGA FRAME_QHD FRAME_WQXGA + # # FRAME_P_FHD FRAME_QSXGA + # # Check this link for more information: https://bit.ly/2YOzizz + # + # # special effects + # camera.speffect(camera.EFFECT_NONE) + # # The options are the following: + # # EFFECT_NONE (default) EFFECT_NEG EFFECT_BW EFFECT_RED EFFECT_GREEN EFFECT_BLUE EFFECT_RETRO + # + # # white balance + # camera.whitebalance(camera.WB_NONE) + # # The options are the following: + # # WB_NONE (default) WB_SUNNY WB_CLOUDY WB_OFFICE WB_HOME + # + # # saturation + # camera.saturation(0) + # # -2,2 (default 0). -2 grayscale + # + # # brightness + # camera.brightness(0) + # # -2,2 (default 0). 2 brightness + # + # # contrast + # camera.contrast(0) + # #-2,2 (default 0). 2 highcontrast + # + # # quality + # camera.quality(10) + # # 10-63 lower number means higher quality + # + + buf = camera.capture() + + if buf: + print("Length of buf:", len(buf)) + + if fn: + with open(fn, "wb") as f: + f.write(buf) + else: + print("not written - no filename given") + # print("Contents of buf in hex:", buf.hex()) + + else: + print("Capture failed (too big for PSRAM?") + + # print("open http://esp32-cam-05.local/foo.jpg") + + camera.deinit() + + +try: + # if 'ARGV' in locals(): + eval( + "capture(ARGV)" + ) # ARGV is supplied by caller thusly: ARGV=["pic.jpg","5","10"];exec(open("bin/photo.py").read()) +except: # Exception as e: + # print(e) # name 'ARGV' isn't defined + capture([]) diff --git a/ports/esp32/boards/ESP32_CAM/sdkconfig.d2wd b/ports/esp32/boards/ESP32_CAM/sdkconfig.d2wd new file mode 100644 index 0000000000..2ac983693d --- /dev/null +++ b/ports/esp32/boards/ESP32_CAM/sdkconfig.d2wd @@ -0,0 +1,17 @@ +# Optimise using -Os to reduce size +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_COMPILER_OPTIMIZATION_PERF=n +CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT=y + +# Change maximum log level to error, to reduce firmware size. +CONFIG_LOG_MAXIMUM_LEVEL_ERROR=y +CONFIG_LOG_MAXIMUM_LEVEL_INFO=n + +# Disable SPI Ethernet driver to reduce firmware size. +CONFIG_ETH_USE_SPI_ETHERNET=n + +CONFIG_ESPTOOLPY_FLASHMODE_DIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y +CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions-2MiB.csv" diff --git a/ports/esp32/boards/ESP32_CAM/sdkconfig.ota b/ports/esp32/boards/ESP32_CAM/sdkconfig.ota new file mode 100644 index 0000000000..352dd96f23 --- /dev/null +++ b/ports/esp32/boards/ESP32_CAM/sdkconfig.ota @@ -0,0 +1,7 @@ +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions-4MiB-ota.csv" + +# Reduce firmware size to fit in the OTA partition. +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_COMPILER_OPTIMIZATION_PERF=n +CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT=y diff --git a/ports/esp32/boards/ESP32_CAM/sdkconfig.unicore b/ports/esp32/boards/ESP32_CAM/sdkconfig.unicore new file mode 100644 index 0000000000..f0b0b5e03d --- /dev/null +++ b/ports/esp32/boards/ESP32_CAM/sdkconfig.unicore @@ -0,0 +1 @@ +CONFIG_FREERTOS_UNICORE=y diff --git a/tools/pyboard.py b/tools/pyboard.py index c422b64ac5..4572d21ebf 100755 --- a/tools/pyboard.py +++ b/tools/pyboard.py @@ -265,7 +265,14 @@ class ProcessPtyToTerminal: class Pyboard: def __init__( - self, device, baudrate=115200, user="micro", password="python", wait=0, exclusive=True + self, + device, + baudrate=115200, + user="micro", + password="python", + wait=0, + exclusive=True, + hard_reset=False, ): self.in_raw_repl = False self.use_raw_paste = True @@ -281,7 +288,7 @@ class Pyboard: import serial.tools.list_ports # Set options, and exclusive if pyserial supports it - serial_kwargs = {"baudrate": baudrate, "interCharTimeout": 1} + serial_kwargs = {"baudrate": baudrate, "interCharTimeout": 1, "rtscts": 0, "dsrdtr": 0} if serial.__version__ >= "3.3": serial_kwargs["exclusive"] = exclusive @@ -300,7 +307,28 @@ class Pyboard: self.serial.rts = False # RTS False = EN High = MCU enabled self.serial.open() else: - self.serial = serial.Serial(device, **serial_kwargs) + #self.serial = serial.Serial(device, **serial_kwargs) + #Cannot do serial.Serial(device...) because below is only way to block a high-pulse on rts + self.serial = serial.Serial() + self.serial.dtr = False # DTR False = gpio0 High = Normal boot + self.serial.rts = False # RTS False = EN High = MCU enabled + self.serial.port = device + self.serial.baudrate = serial_kwargs["baudrate"] + self.serial.rtscts = 0 + self.serial.dsrdtr = 0 + self.serial.inter_byte_timeout = serial_kwargs["interCharTimeout"] + self.serial.exclusive = serial_kwargs["exclusive"] + self.serial.open() + + + if hard_reset: + time.sleep(0.2) + # this is reset (setting this "high" resets the MCU) + self.serial.rts = True + time.sleep(0.2) + self.serial.rts = False + # must wait for the reset, otherwise the ctrl-A gets lost + time.sleep(2.0) break except (OSError, IOError): # Py2 and Py3 have different errors if wait == 0: @@ -325,7 +353,10 @@ class Pyboard: # if data_consumer is used then data is not accumulated and the ending must be 1 byte long assert data_consumer is None or len(ending) == 1 - data = self.serial.read(min_num_bytes) + if min_num_bytes > 0: + data = self.serial.read(min_num_bytes) + else: + data = b"" if data_consumer: data_consumer(data) timeout_count = 0 @@ -356,10 +387,17 @@ class Pyboard: self.serial.read(n) n = self.serial.inWaiting() - self.serial.write(b"\r\x01") # ctrl-A: enter raw REPL + retry = 10 + while retry > 0: # resend every 1s (sends get lost while resetting) + retry = retry - 1 + self.serial.write(b"\r\x01\x01") # ctrl-A: enter raw REPL (needs 2) + data = self.read_until(0, b"raw REPL; CTRL-B to exit\r\n>", timeout=1) + if data.endswith(b"raw REPL; CTRL-B to exit\r\n>"): + retry = 0 + else: + time.sleep(0.1) if soft_reset: - data = self.read_until(1, b"raw REPL; CTRL-B to exit\r\n>") if not data.endswith(b"raw REPL; CTRL-B to exit\r\n>"): print(data) raise PyboardError("could not enter raw repl") @@ -374,7 +412,7 @@ class Pyboard: print(data) raise PyboardError("could not enter raw repl") - data = self.read_until(1, b"raw REPL; CTRL-B to exit\r\n") + data = self.read_until(1, b"raw REPL; CTRL-B to exit\r\n", timeout=1) if not data.endswith(b"raw REPL; CTRL-B to exit\r\n"): print(data) raise PyboardError("could not enter raw repl") @@ -783,6 +821,12 @@ def main(): cmd_parser.add_argument("-u", "--user", default="micro", help="the telnet login username") cmd_parser.add_argument("-p", "--password", default="python", help="the telnet login password") cmd_parser.add_argument("-c", "--command", help="program passed in as string") + cmd_parser.add_argument( + "--hard-reset", + action="store_true", + dest="hard_reset", + help="Perform a hard reset when connecting to the board (requires your serial programmer to properly wire RTS pin to reset)", + ) cmd_parser.add_argument( "-w", "--wait", @@ -801,6 +845,7 @@ def main(): "--no-soft-reset", action="store_false", dest="soft_reset", + help="Whether to perform a soft reset when connecting to the board [default]", ) group = cmd_parser.add_mutually_exclusive_group() group.add_argument( @@ -839,7 +884,13 @@ def main(): # open the connection to the pyboard try: pyb = Pyboard( - args.device, args.baudrate, args.user, args.password, args.wait, args.exclusive + args.device, + args.baudrate, + args.user, + args.password, + args.wait, + args.exclusive, + hard_reset=args.hard_reset, ) except PyboardError as er: print(er)