diff --git a/adafruit_ov2640.py b/adafruit_ov2640.py index f94ed34..87d556a 100644 --- a/adafruit_ov2640.py +++ b/adafruit_ov2640.py @@ -106,6 +106,7 @@ OV2640_COLOR_RGB = 0 OV2640_COLOR_YUV = 1 +OV2640_COLOR_JPEG = 2 _IMAGE_MODE_Y8_DVP_EN = const(0x40) _IMAGE_MODE_JPEG_EN = const(0x10) @@ -886,55 +887,70 @@ ] ) -# _ov2640_settings_jpeg3 = bytes([ -# _BANK_SEL, _BANK_DSP, -# _RESET, _RESET_JPEG | _RESET_DVP, -# _IMAGE_MODE, _IMAGE_MODE_JPEG_EN | _IMAGE_MODE_HREF_VSYNC, -# 0xD7, 0x03, -# 0xE1, 0x77, -# 0xE5, 0x1F, -# 0xD9, 0x10, -# 0xDF, 0x80, -# 0x33, 0x80, -# 0x3C, 0x10, -# 0xEB, 0x30, -# 0xDD, 0x7F, -# _RESET, 0x00, -# ]) - -_ov2640_settings_yuv422 = bytes( - [ - _BANK_SEL, - _BANK_DSP, - _RESET, - _RESET_DVP, - _IMAGE_MODE, - _IMAGE_MODE_YUV422, - 0xD7, - 0x01, - 0xE1, - 0x67, - _RESET, - 0x00, - ] -) - -_ov2640_settings_rgb565 = bytes( - [ - _BANK_SEL, - _BANK_DSP, - _RESET, - _RESET_DVP, - _IMAGE_MODE, - _IMAGE_MODE_RGB565, - 0xD7, - 0x03, - 0xE1, - 0x77, - _RESET, - 0x00, - ] -) +_ov2640_color_settings = { + OV2640_COLOR_JPEG: bytes( + [ + _BANK_SEL, + _BANK_DSP, + _RESET, + _RESET_JPEG | _RESET_DVP, + _IMAGE_MODE, + _IMAGE_MODE_JPEG_EN | _IMAGE_MODE_HREF_VSYNC, + 0xD7, + 0x03, + 0xE1, + 0x77, + 0xE5, + 0x1F, + 0xD9, + 0x10, + 0xDF, + 0x80, + 0x33, + 0x80, + 0x3C, + 0x10, + 0xEB, + 0x30, + 0xDD, + 0x7F, + _RESET, + 0x00, + ] + ), + OV2640_COLOR_YUV: bytes( + [ + _BANK_SEL, + _BANK_DSP, + _RESET, + _RESET_DVP, + _IMAGE_MODE, + _IMAGE_MODE_YUV422, + 0xD7, + 0x01, + 0xE1, + 0x67, + _RESET, + 0x00, + ] + ), + OV2640_COLOR_RGB: bytes( + [ + _BANK_SEL, + _BANK_DSP, + _RESET, + _RESET_DVP, + _IMAGE_MODE, + _IMAGE_MODE_RGB565, + 0xD7, + 0x03, + 0xE1, + 0x77, + _RESET, + 0x00, + ] + ), +} class _RegBits: @@ -1114,6 +1130,19 @@ def capture(self, buf): captured image. Note that this can be a ulab array or a displayio Bitmap. """ self._imagecapture.capture(buf) + if self.colorspace == OV2640_COLOR_JPEG: + eoi = buf.find(b"\xff\xd9") + if eoi != -1: + # terminate the JPEG data just after the EOI marker + return memoryview(buf)[: eoi + 2] + return None + + @property + def capture_buffer_size(self): + """Return the size of capture buffer to use with current resolution & colorspace settings""" + if self.colorspace == OV2640_COLOR_JPEG: + return self.width * self.height // 5 + return self.width * self.height * 2 @property def mclk_frequency(self): @@ -1140,17 +1169,15 @@ def colorspace(self): @colorspace.setter def colorspace(self, colorspace): self._colorspace = colorspace - self._write_list( - _ov2640_settings_rgb565 - if colorspace == OV2640_COLOR_RGB - else _ov2640_settings_yuv422 - ) + self._set_size_and_colorspace() + + def _set_colorspace(self): + colorspace = self._colorspace + settings = _ov2640_color_settings[colorspace] + + self._write_list(settings) # written twice? - self._write_list( - _ov2640_settings_rgb565 - if colorspace == OV2640_COLOR_RGB - else _ov2640_settings_yuv422 - ) + self._write_list(settings) time.sleep(0.01) def deinit(self): @@ -1168,8 +1195,8 @@ def size(self): """Get or set the captured image size, one of the ``OV2640_SIZE_`` constants.""" return self._size - @size.setter - def size(self, size): + def _set_size_and_colorspace(self): + size = self._size width, height, ratio = _resolution_info[size] offset_x, offset_y, max_x, max_y = _ratio_table[ratio] mode = _OV2640_MODE_UXGA @@ -1190,7 +1217,11 @@ def size(self, size): offset_y //= 2 self._set_window(mode, offset_x, offset_y, max_x, max_y, width, height) + + @size.setter + def size(self, size): self._size = size + self._set_size_and_colorspace() def _set_flip(self): bits = 0 @@ -1267,15 +1298,19 @@ def _set_window( ((height >> 6) & 0x04) | ((width >> 8) & 0x03), ] - pclk_auto = 1 + pclk_auto = 0 pclk_div = 8 clk_2x = 0 - clk_div = 7 + clk_div = 0 + + if self._colorspace != OV2640_COLOR_JPEG: + pclk_auto = 1 + clk_div = 7 if mode == _OV2640_MODE_CIF: regs = _ov2640_settings_to_cif - # if pixformat is not jpeg: - clk_div = 3 + if self._colorspace != OV2640_COLOR_JPEG: + clk_div = 3 elif mode == _OV2640_MODE_SVGA: regs = _ov2640_settings_to_svga else: @@ -1294,7 +1329,7 @@ def _set_window( time.sleep(0.01) # Reestablish colorspace - self.colorspace = self._colorspace + self._set_colorspace() # Reestablish test pattern if self._test_pattern: diff --git a/docs/examples.rst b/docs/examples.rst index 11e897b..1369773 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -6,3 +6,39 @@ Ensure your device works with this simple test. .. literalinclude:: ../examples/ov2640_simpletest.py :caption: examples/ov2640_simpletest.py :linenos: + +Display an image from the camera on the Kaluga 1.3 board, if it is fitted with an ili9341 display. + +.. literalinclude:: ../examples/ov2640_displayio_kaluga1_3_ili9341.py + :caption: ../examples/ov2640_displayio_kaluga1_3_ili9341.py + :linenos: + +Display an image from the camera on the Kaluga 1.3 board, if it is fitted with an st7789 display. + +.. literalinclude:: ../examples/ov2640_displayio_kaluga1_3_st7789.py + :caption: ../examples/ov2640_displayio_kaluga1_3_st7789.py + :linenos: + +Display an image from the camera connected to a Raspberry Pi Pico with an st7789 2" display + +.. literalinclude:: ../examples/ov2640_displayio_pico_st7789_2in.py + :caption: ../examples/ov2640_displayio_pico_st7789_2in.py + :linenos: + +Save an image to internal flash on Kaluga 1.3 + +.. literalinclude:: ../examples/ov2640_jpeg_kaluga1_3.py + :caption: ../examples/ov2640_jpeg_kaluga1_3.py + :linenos: + +``boot.py`` for the above program + +.. literalinclude:: ../examples/ov2640_jpeg_kaluga1_3_boot.py + :caption: ../examples/ov2640_jpeg_kaluga1_3_boot.py + :linenos: + +Preview images on LCD then save to SD on Kaluga 1.3 fitted with an ili9341 display + +.. literalinclude:: ../examples/ov2640_jpeg_sd_kaluga1_3.py + :caption: ../examples/ov2640_jpeg_sd_kaluga1_3.py + :linenos: diff --git a/examples/ov2640_displayio_kaluga1_3_ili9341.py b/examples/ov2640_displayio_kaluga1_3_ili9341.py index cf4a0a5..41d4170 100644 --- a/examples/ov2640_displayio_kaluga1_3_ili9341.py +++ b/examples/ov2640_displayio_kaluga1_3_ili9341.py @@ -12,9 +12,6 @@ rotation=90! This demo is for the ili9341. If the display is garbled, try adding rotation=90, or try modifying it to use ST7799. -The camera included with the Kaluga development kit is the incompatible OV2640, -it won't work. - The audio board must be mounted between the Kaluga and the LCD, it provides the I2C pull-ups(!) """ diff --git a/examples/ov2640_displayio_kaluga1_3_st7789.py b/examples/ov2640_displayio_kaluga1_3_st7789.py index fdbd4a5..5c407e4 100644 --- a/examples/ov2640_displayio_kaluga1_3_st7789.py +++ b/examples/ov2640_displayio_kaluga1_3_st7789.py @@ -13,9 +13,6 @@ of straight lines, it may be an ili9341. If it has a bunch of wiggly traces, it may be an st7789. If in doubt, try both demos. -The camera included with the Kaluga development kit is the incompatible OV2640, -it won't work. - The audio board must be mounted between the Kaluga and the LCD, it provides the I2C pull-ups(!) """ diff --git a/examples/ov2640_jpeg_kaluga1_3.py b/examples/ov2640_jpeg_kaluga1_3.py new file mode 100644 index 0000000..5d39cbe --- /dev/null +++ b/examples/ov2640_jpeg_kaluga1_3.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +""" +The Kaluga development kit comes in two versions (v1.2 and v1.3); this demo is +tested on v1.3. + +The audio board must be mounted between the Kaluga and the LCD, it provides the +I2C pull-ups(!) + +You also need to place ov2640_jpeg_kaluga1_3_boot.py at CIRCUITPY/boot.py +and reset the board to make the internal flash readable by CircuitPython. +You can make CIRCUITPY readable from your PC by booting CircuitPython in +safe mode or holding the "MODE" button on the audio daughterboard while +powering on or resetting the board. +""" + +import board +import busio +import adafruit_ov2640 + + +bus = busio.I2C(scl=board.CAMERA_SIOC, sda=board.CAMERA_SIOD) +cam = adafruit_ov2640.OV2640( + bus, + data_pins=board.CAMERA_DATA, + clock=board.CAMERA_PCLK, + vsync=board.CAMERA_VSYNC, + href=board.CAMERA_HREF, + mclk=board.CAMERA_XCLK, + mclk_frequency=20_000_000, + size=adafruit_ov2640.OV2640_SIZE_QVGA, +) + +pid = cam.product_id +ver = cam.product_version +print(f"Detected pid={pid:x} ver={ver:x}") +# cam.test_pattern = True + +cam.colorspace = adafruit_ov2640.OV2640_COLOR_JPEG +b = bytearray(cam.capture_buffer_size) +jpeg = cam.capture(b) + +print(f"Captured {len(jpeg)} bytes of jpeg data") +try: + with open("/jpeg.jpg", "wb") as f: + f.write(jpeg) +except OSError as e: + print(e) + print( + "A 'read-only filesystem' error occurs if you did not correctly install\nov2640_jpeg_kaluga1_3_boot.py as CIRCUITPY/boot.py and reset the board" + ) +print("Wrote to CIRCUITPY/jpeg.jpg") diff --git a/examples/ov2640_jpeg_kaluga1_3_boot.py b/examples/ov2640_jpeg_kaluga1_3_boot.py new file mode 100644 index 0000000..2a872fb --- /dev/null +++ b/examples/ov2640_jpeg_kaluga1_3_boot.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense +"""Use this file as CIRCUITPY/boot.py in conjunction with ov2640_jpeg_kaluga1_3.py + +It makes the CIRCUITPY filesystem writable to CircuitPython +(and read-only to the PC) unless the "MODE" button on the audio +daughterboard is held while the board is powered on or reset. +""" + +import analogio +import board +import storage + +V_MODE = 1.98 +V_RECORD = 2.41 + +a = analogio.AnalogIn(board.IO6) +a_voltage = a.value * a.reference_voltage / 65535 +if abs(a_voltage - V_MODE) > 0.05: # If mode is NOT pressed... + print("storage writable by CircuitPython") + storage.remount("/", readonly=False) diff --git a/examples/ov2640_jpeg_sd_kaluga1_3.py b/examples/ov2640_jpeg_sd_kaluga1_3.py new file mode 100644 index 0000000..5e449fc --- /dev/null +++ b/examples/ov2640_jpeg_sd_kaluga1_3.py @@ -0,0 +1,146 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +""" +Display an image on the LCD, then record an image when the REC button is pressed/held. + +The Kaluga development kit comes in two versions (v1.2 and v1.3); this demo is +tested on v1.3. + +The audio board must be mounted between the Kaluga and the LCD, it provides the +I2C pull-ups(!) + +The v1.3 development kit's LCD can have one of two chips, the ili9341 or +st7789. Furthermore, there are at least 2 ILI9341 variants, one of which needs +rotation=90! This demo is for the ili9341. If the display is garbled, try adding +rotation=90, or try modifying it to use ST7799. + +This example also requires an SD card breakout wired as follows: + * IO18: SD Clock Input + * IO17: SD Serial Output (MISO) + * IO14: SD Serial Input (MOSI) + * IO12: SD Chip Select + +Insert a CircuitPython-compatible SD card before powering on the Kaluga. +Press the "Record" button on the audio daughterboard to take a photo. +""" + +import os + +import analogio +import board +import busio +import displayio +import sdcardio +import storage +from adafruit_ili9341 import ILI9341 +import adafruit_ov2640 + +V_MODE = 1.98 +V_RECORD = 2.41 + +a = analogio.AnalogIn(board.IO6) + +# Release any resources currently in use for the displays +displayio.release_displays() + +spi = busio.SPI(MOSI=board.LCD_MOSI, clock=board.LCD_CLK) +display_bus = displayio.FourWire( + spi, command=board.LCD_D_C, chip_select=board.LCD_CS, reset=board.LCD_RST +) +display = ILI9341(display_bus, width=320, height=240, rotation=90) + +bus = busio.I2C(scl=board.CAMERA_SIOC, sda=board.CAMERA_SIOD) +cam = adafruit_ov2640.OV2640( + bus, + data_pins=board.CAMERA_DATA, + clock=board.CAMERA_PCLK, + vsync=board.CAMERA_VSYNC, + href=board.CAMERA_HREF, + mclk=board.CAMERA_XCLK, + mclk_frequency=20_000_000, + size=adafruit_ov2640.OV2640_SIZE_QVGA, +) + +cam.flip_x = False +cam.flip_y = True +pid = cam.product_id +ver = cam.product_version +print(f"Detected pid={pid:x} ver={ver:x}") +# cam.test_pattern = True + +g = displayio.Group(scale=1) +bitmap = displayio.Bitmap(320, 240, 65536) +tg = displayio.TileGrid( + bitmap, + pixel_shader=displayio.ColorConverter( + input_colorspace=displayio.Colorspace.RGB565_SWAPPED + ), +) +g.append(tg) +display.show(g) + +display.auto_refresh = False + +sd_spi = busio.SPI(clock=board.IO18, MOSI=board.IO14, MISO=board.IO17) +sd_cs = board.IO12 +sdcard = sdcardio.SDCard(sd_spi, sd_cs) +vfs = storage.VfsFat(sdcard) +storage.mount(vfs, "/sd") + + +def exists(filename): + try: + os.stat(filename) + return True + except OSError as e: + return False + + +_image_counter = 0 + + +def open_next_image(): + global _image_counter + while True: + filename = f"/sd/img{_image_counter:04d}.jpg" + _image_counter += 1 + if exists(filename): + continue + print("#", filename) + return open(filename, "wb") + + +def capture_image(): + old_size = cam.size + old_colorspace = cam.colorspace + + try: + cam.size = adafruit_ov2640.OV2640_SIZE_UXGA + cam.colorspace = adafruit_ov2640.OV2640_COLOR_JPEG + b = bytearray(cam.capture_buffer_size) + jpeg = cam.capture(b) + + print(f"Captured {len(jpeg)} bytes of jpeg data") + with open_next_image() as f: + f.write(jpeg) + finally: + cam.size = old_size + cam.colorspace = old_colorspace + + +def main(): + display.auto_refresh = False + while True: + a_voltage = a.value * a.reference_voltage / 65535 + record_pressed = abs(a_voltage - V_RECORD) < 0.05 + if record_pressed: + capture_image() + cam.capture(bitmap) + bitmap.dirty() + display.refresh(minimum_frames_per_second=0) + + +main()