From 33cd166e036d119d2f1ccc9db6d786f897c3f232 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Wed, 19 Nov 2025 13:30:15 -0700 Subject: [PATCH 1/3] Improve RP2 DVP PIO driver Fixes #1 Now buffers each row of pixels in SRAM if needed. DMA setup is similar to HSTX DVI driver --- red_vision/cameras/dvp_rp2_pio.py | 521 +++++++++++++++++++++++++----- red_vision/cameras/hm01b0.py | 1 + red_vision/cameras/hm01b0_pio.py | 9 +- red_vision/cameras/ov5640.py | 5 +- red_vision/cameras/ov5640_pio.py | 18 +- 5 files changed, 471 insertions(+), 83 deletions(-) diff --git a/red_vision/cameras/dvp_rp2_pio.py b/red_vision/cameras/dvp_rp2_pio.py index 81d2840..fb779fb 100644 --- a/red_vision/cameras/dvp_rp2_pio.py +++ b/red_vision/cameras/dvp_rp2_pio.py @@ -16,7 +16,9 @@ #------------------------------------------------------------------------------- import rp2 +import array from machine import Pin, PWM +from uctypes import addressof class DVP_RP2_PIO(): """ @@ -34,7 +36,7 @@ def __init__( xclk_freq, sm_id, num_data_pins, - bytes_per_frame, + bytes_per_pixel, byte_swap ): """ @@ -49,17 +51,18 @@ def __init__( xclk_freq (int): Frequency in Hz for the external clock sm_id (int): PIO state machine ID num_data_pins (int): Number of data pins used in DVP interface - bytes_per_frame (int): Number of bytes per frame to capture + bytes_per_pixel (int): Number of bytes per pixel byte_swap (bool): Whether to swap bytes in the captured data """ + # Store pin assignments self._pin_d0 = pin_d0 self._pin_vsync = pin_vsync self._pin_hsync = pin_hsync self._pin_pclk = pin_pclk self._pin_xclk = pin_xclk - self._sm_id = sm_id # Initialize DVP pins as inputs + self._num_data_pins = num_data_pins for i in range(num_data_pins): Pin(pin_d0+i, Pin.IN) Pin(pin_vsync, Pin.IN) @@ -72,6 +75,18 @@ def __init__( self._xclk.freq(xclk_freq) self._xclk.duty_u16(32768) # 50% duty cycle + # Store transfer parameters + self._bytes_per_pixel = bytes_per_pixel + self._byte_swap = byte_swap + + # Set up the PIO state machine + self._sm_id = sm_id + self._setup_pio() + + # Set up the DMA controllers + self._setup_dmas() + + def _setup_pio(self): # Copy the PIO program program = self._pio_read_dvp @@ -81,102 +96,462 @@ def __init__( program[0][3] |= self._pin_pclk & 0x1F # Mask in the number of data pins - program[0][2] &= 0xFFFFFFE0 - program[0][2] |= num_data_pins + program[0][2] |= self._num_data_pins # Create PIO state machine to capture DVP data self._sm = rp2.StateMachine( self._sm_id, program, - in_base = pin_d0 + in_base = self._pin_d0, + push_thresh = self._bytes_per_pixel * 8 ) - # Create DMA controller to transfer data from PIO to buffer - self._dma = rp2.DMA() - req_num = ((self._sm_id // 4) << 3) + (self._sm_id % 4) + 4 - bytes_per_transfer = 4 - dma_ctrl = self._dma.pack_ctrl( - # 0 = 1 byte, 1 = 2 bytes, 2 = 4 bytes - size = {1:0, 2:1, 4:2}[bytes_per_transfer], - inc_read = False, - treq_sel = req_num, - bswap = byte_swap - ) - self._dma.config( - read = self._sm, - count = bytes_per_frame // bytes_per_transfer, - ctrl = dma_ctrl + # Here is the PIO program, which is configurable to mask in the GPIO pins + # and the number of data pins. It must be configured before the state + # machine is created + @rp2.asm_pio( + in_shiftdir = rp2.PIO.SHIFT_LEFT, + autopush = True, + fifo_join = rp2.PIO.JOIN_RX ) + def _pio_read_dvp(): + """ + PIO program to read DVP data from the GPIO pins. + """ + wait(1, gpio, 0) # Mask in HSYNC pin + wait(1, gpio, 0) # Mask in PCLK pin + in_(pins, 32) # Mask in number of pins + wait(0, gpio, 0) # Mask in PCLK pin - def _active(self, active=None): + def _is_in_sram(self, data_addr): """ - Sets or gets the active state of the DVP interface. + Checks whether a given memory address is in SRAM. Args: - active (bool, optional): - - True: Activate the DVP interface - - False: Deactivate the DVP interface - - None: Get the current active state - + data_addr (int): Memory address to check Returns: - bool: Current active state if no argument is provided + bool: True if address is in SRAM, False otherwise """ - # If no argument is provided, return the current active state - if active == None: - return self._sm.active() + # SRAM address range. + SRAM_BASE = 0x20000000 + total_sram_size = 520*1024 # 520 KB - # Disable the DMA, the VSYNC handler will re-enable it when needed - self._dma.active(False) + # Return whether address is in SRAM. + return data_addr >= SRAM_BASE and data_addr < SRAM_BASE + total_sram_size - # Set the active state of the state machine - self._sm.active(active) + def _setup_dmas(self): + """ + Sets up the DMA controllers for the DVP interface. + """ + # This driver uses multiple DMA channels to transfer pixel data from the + # camera to the image buffer. A detailed explanation and motive for this + # DMA structure is better explained in the RP2 HSTX DVI driver, but an + # abbreviated summary is provided here, along with some diagrams and + # differences specific to the DVP interface. + # + # One DMA channel (the "dispatcher") reads from a sequence of "control + # blocks", and writes them to the control registers of a second DMA + # channel (the "executer"). This pair of DMA channels continually + # trigger and reconfigure each other, and are capable of transferring + # data between any memory locations (including reconfiguring other + # peripherals by writing their control registers) with zero CPU overhead + # after initial configuration. + # + # If the image buffer is in SRAM, the executer just transfers an entire + # frame of data from the PIO RX FIFO to the image buffer with a single + # control block. The second control block then makes the executer write + # a third "nested" control block to the dispatcher, reconfiguring it to + # start the next frame, resulting in continuous video capture. + # + # +--------------------------+ + # |+------------+ +------+| +-------+ + # || Row Buffer |<---|Pixels||<-----| Image | + # |+------------+ ^ +------+| DVP +-------+ + # || Ctrl Blocks| | PIO FIFO| Camera + # |+------------+ | | + # | SRAM | | | + # | | +------------+| + # | | | Executer || + # | | +------------+| + # | | ^ | + # | | | | + # | | +------------+| + # | +->| Dispatcher || + # | +------------+| + # +--------------------------+ + # RP2350 + # + # If the image buffer is instead in PSRAM, transferring directly from + # the PIO FIFO to PSRAM is not reliable, because some cameras (eg. the + # OV5640) output each row of pixels in very short bursts that can exceed + # the QSPI bus transfer speed. So instead, the executer DMA transfers + # each row of pixel data to a small buffer in SRAM, then it triggers a + # third DMA channel (the "streamer") with another "nested" control block + # to transfer the row from SRAM to PSRAM as fast as the QSPI bus can + # handle. Both transfers in and out of the row buffer can happen at the + # same time, but the streamer starts earlier and stays ahead as long as + # the camera's average data speed is not too fast. + # + # The normal XIP memory mapped interface adds a lot of latency (see + # section 4.4.3 of the RP2350 datasheet), so it could be beneficial to + # use the QMI direct mode for the SRAM-to-PSRAM transfer (see section + # 12.14.5 of the RP2350 datasheet). But the "bursty" cameras usually + # have enough delay between each row that the normal memory mapped + # interface is sufficient. The QMI direct mode also *dramatically* + # complicates things, so this driver just uses the normal memory mapped + # interface for simplicity. + # + # +--------------------------------------+ + # +-----+ |+------+ +------------+ +------+| +-------+ + # |Image|<-----||Pixels|<---| Row Buffer |<---|Pixels||<-----| Image | + # +-----+ QSPI |+------+ ^ +------------+ ^ +------+| DVP +-------+ + # PSRAM | XIP | | Ctrl Blocks| | PIO FIFO| Camera + # | | +------------+ | | + # | | SRAM | | | + # | +------------+ | +------------+| + # | | Streamer |<--------| Executer || + # | +------------+ | +------------+| + # | | ^ | + # | | | | + # | | +------------+| + # | +->| Dispatcher || + # | +------------+| + # +--------------------------------------+ + # RP2350 + # + # When the image buffer is in PSRAM, the control block sequence is not + # automatically restarted at the end of each frame. The QSPI bus can end + # up being a real bottleneck if other things need to use it (eg. + # processing other image buffers, display output, flash memory access, + # etc.), so constantly spamming the QSPI bus with camera data can cause + # other things to slow down, or even cause pixels from the camera to be + # dropped if other transfers on the QSPI bus have higher priority. And + # in most real applications, most frames from the camera are ignored + # anyways, so it's better to just capture frames when needed. + + # Create the dispatcher and executer DMA channels. + self._dma_dispatcher = rp2.DMA() + self._dma_executer = rp2.DMA() + + # Check if the display buffer is in PSRAM. + self._buffer_is_in_psram = not self._is_in_sram(addressof(self._buffer)) + + # If the buffer is in PSRAM, create the streamer DMA channel and row + # buffer in SRAM. + if self._buffer_is_in_psram: + # Create the streamer DMA channel. + self._dma_streamer = rp2.DMA() + + # Create the row buffer. + self._bytes_per_row = self._width * self._bytes_per_pixel + self._row_buffer = array.array("I", [0] * (self._bytes_per_row // 4)) + + # Verify row buffer is in SRAM. If not, we'll still have the same + # latency problem. + if not self._is_in_sram(addressof(self._row_buffer)): + raise MemoryError("not enough space in SRAM for row buffer") - # If active, set up the VSYNC interrupt handler - if active: - Pin(self._pin_vsync).irq( - trigger = Pin.IRQ_FALLING, - handler = lambda pin: self._vsync_handler() + # Create DMA control register values. + self._create_dma_ctrl_registers() + + # Create DMA control blocks. + self._create_control_blocks() + + # Assemble the control blocks in order. + self._assemble_control_blocks() + + def _create_dma_ctrl_registers(self): + """ + Creates the DMA control register values. + """ + # DMA DREQ (data request) signal selections for the RP2350 to pace + # transfers by the peripheral data request signals. For some reason, + # these are not defined in the `rp2.DMA` class. + # + # According to section 12.6.4.1 of the RP2350 datasheet, the PIO RX DREQ + # indices are determined by the PIO number (n) and state machine number + # (m) as follows: + # + # DREQ_PIOn_RXm = (n * 8) + 4 + m + pio_num = self._sm_id // 4 + sm_num = self._sm_id % 4 + DREQ_PIO_RX = (pio_num * 8) + 4 + sm_num # Pace transfers with PIO RX FIFO data request + DREQ_FORCE = 63 # Transfer as fast as possible + + # Dispatcher control register. The "ring" parameters are used to have + # the write address wrap around after 4 transfers (ring_sel = True means + # wrap the write address, and ring_size = 4 specifies a 4-bit address + # wrap, meaning 2**4 bits = 16 bits = 4 words), so the dispatcher + # continuously re-writes the 4 control registers of the executer. + self._dma_ctrl_cb_dispatcher = self._dma_dispatcher.pack_ctrl( + # size = 2, + # inc_read = True, + # inc_write = True, + ring_size = 4, + ring_sel = True, + # chain_to = self._dma_dispatcher.channel, + # treq_sel = DREQ_FORCE, + bswap = False, + ) + + # Executer control register for getting pixel data from the PIO FIFO. It + # transfers one pixel at a time (1, 2, or 4 bytes) and swaps bytes if + # needed. Once done, it chains back to the dispatcher to get the next + # control block. + self._dma_ctrl_pio_repeat = self._dma_executer.pack_ctrl( + size = {1:0, 2:1, 4:2}[self._bytes_per_pixel], + inc_read = False, + inc_write = True, + # ring_size = 0, + # ring_sel = False, + chain_to = self._dma_dispatcher.channel, + treq_sel = DREQ_PIO_RX, + bswap = self._byte_swap, + ) + + # Executer control register for sending nested control block to another + # DMA channel without chaining back to the dispatcher. + self._dma_ctrl_cb_executer_nested_single = self._dma_executer.pack_ctrl( + # size = 2, + # inc_read = True, + # inc_write = True, + # ring_size = 0, + # ring_sel = False, + # chain_to = self._dma_executer.channel, + # treq_sel = DREQ_FORCE, + bswap = False, + ) + + # If the display buffer is in PSRAM, we need additional DMA control + # registers. + if self._buffer_is_in_psram: + # DMA control register for the executer to send a nested control + # block with chaining back to the dispatcher, so another control + # block can be sent immediately. + self._dma_ctrl_cb_executer_nested_repeat = self._dma_executer.pack_ctrl( + # size = 2, + # inc_read = True, + # inc_write = True, + # ring_size = 0, + # ring_sel = False, + chain_to = self._dma_dispatcher.channel, + # treq_sel = DREQ_FORCE, + bswap = False, ) - # If not active, disable the VSYNC interrupt handler - else: - Pin(self._pin_vsync).irq( - handler = None + + # DMA control register for the streamer to transfer a row of pixel + # data from the row buffer in SRAM to PSRAM. + self._dma_ctrl_streamer = self._dma_streamer.pack_ctrl( + # size = 2, + # inc_read = True, + # inc_write = False, + # ring_size = 0, + # ring_sel = False, + # chain_to = self._dma_streamer.channel, + # treq_sel = DREQ_FORCE, + bswap = False, ) - def _vsync_handler(self): + def _create_control_blocks(self): + """ + Creates the DMA control blocks. + """ + # Determine how many control blocks are needed. The control block + # sequence is created in `_assemble_control_blocks()`, but we need to + # create the control block array early so the restart frame block can + # reference it. + num_cb = 0 + if not self._buffer_is_in_psram: + num_cb += 1 # PIO read control block + num_cb += 1 # Restart frame control block + else: + num_cb += self._height # PIO read control blocks + num_cb += self._height # Streamer control blocks + + # There are 4 words per control block. + num_cb *= 4 + + # Create control block array. + self._control_blocks = array.array('I', [0] * num_cb) + + # Below are the individual control block definitions, to be written to + # the alias 0 registers of each DMA channel (see section 12.6.3.1 of the + # RP2350 datasheet). The registers are: + # + # 1) READ_ADDR + # 2) WRITE_ADDR + # 3) TRANS_COUNT + # 4) CTRL_TRIG + # + # When CTRL_TRIG is written, that DMA channel immediately starts. + + # Conveniently gets the RX FIFO address instead of TX + pio_rx_fifo_addr = addressof(self._sm) + + # Control blocks are different depending on whether the buffer is in + # SRAM or PSRAM. + if not self._buffer_is_in_psram: + # Control block for executer to read entire frame from PIO RX FIFO + # to image buffer. + self._cb_pio_repeat = array.array('I', [ + pio_rx_fifo_addr, # READ_ADDR + addressof(self._buffer), # WRITE_ADDR + self._width * self._height, # TRANS_COUNT + self._dma_ctrl_pio_repeat, # CTRL_TRIG + ]) + + # Control blocks for restarting the dispatcher. `_cb_restart_frame` + # must be the last control block in the sequence, which causes the + # executer to write the nested `_cb_restart_frame_nested` control + # block back to the dispatcher DMA registers, restarting it from the + # beginning of the control block sequence. + self._cb_restart_frame_nested = array.array('I', [ + addressof(self._control_blocks), # READ_ADDR + addressof(self._dma_executer.registers), # WRITE_ADDR + 4, # TRANS_COUNT + self._dma_ctrl_cb_dispatcher, # CTRL_TRIG + ]) + self._cb_restart_frame = array.array('I', [ + addressof(self._cb_restart_frame_nested), # READ_ADDR + addressof(self._dma_dispatcher.registers), # WRITE_ADDR + len(self._cb_restart_frame_nested), # TRANS_COUNT + self._dma_ctrl_cb_executer_nested_single, # CTRL_TRIG + ]) + else: + # Control block for executer to read 1 row from PIO RX FIFO to image + # buffer. + self._cb_pio_repeat = array.array('I', [ + pio_rx_fifo_addr, # READ_ADDR + addressof(self._row_buffer), # WRITE_ADDR + self._bytes_per_row // self._bytes_per_pixel, # TRANS_COUNT + self._dma_ctrl_pio_repeat, # CTRL_TRIG + ]) + + # Control blocks for the streamer DMA. `_cb_psram_repeat` and + # `_cb_psram_single` cause the executer to write the nested + # `_cb_psram_nested` control block to the streamer DMA registers, + # triggering it to transfer the row buffer to PSRAM. Only the + # `READ_ADDR_TRIG` register is used, since it's the only field that + # needs to change. We do not want to change the write address (let + # it keep incrementing to fill the whole image buffer), nor do we + # need to change the transfer count or control register. + self._cb_psram_nested = array.array('I', [ + addressof(self._row_buffer), # READ_ADDR_TRIG + ]) + self._cb_psram_repeat = array.array('I', [ + addressof(self._cb_psram_nested), # READ_ADDR + addressof(self._dma_streamer.registers[15:16]), # WRITE_ADDR + len(self._cb_psram_nested), # TRANS_COUNT + self._dma_ctrl_cb_executer_nested_repeat, # CTRL_TRIG + ]) + self._cb_psram_single = array.array('I', [ + addressof(self._cb_psram_nested), # READ_ADDR + addressof(self._dma_streamer.registers[15:16]), # WRITE_ADDR + len(self._cb_psram_nested), # TRANS_COUNT + self._dma_ctrl_cb_executer_nested_single, # CTRL_TRIG + ]) + + def _assemble_control_blocks(self): """ - Handles the VSYNC interrupt to capture a frame of data. + Assembles the complete control block sequence to send the image buffer + over DVI, which includes sending all timing signals and pixel data to + the HSTX, starting the XIP stream and PSRAM DMA if needed, and + restarting the control block sequence for the next frame. """ - # Disable DMA before reconfiguring it - self._dma.active(False) + # Reset the control block index. + self._cb_index = 0 - # Reset state machine to ensure ISR is cleared - self._sm.restart() + # Control blocks are different depending on whether the buffer is in + # SRAM or PSRAM. + if not self._buffer_is_in_psram: + # Add control block for executer to read entire frame from PIO RX + # FIFO to image buffer. + self._add_control_block(self._cb_pio_repeat) - # Ensure PIO RX FIFO is empty (it's not emptied by `sm.restart()`) - while self._sm.rx_fifo() > 0: - self._sm.get() + # Add control block to restart by reconfiguring the dispatcher DMA. + self._add_control_block(self._cb_restart_frame) + else: + # Loop through each row of the image. + for row in range(self._height): + # Add control block for executer to read 1 row from PIO RX FIFO + # to image buffer. + self._add_control_block(self._cb_pio_repeat) - # Reset the DMA write address - self._dma.write = self._buffer + # Add control block for streamer to send row from SRAM to PSRAM. + self._add_control_block(self._cb_psram_repeat) - # Start the DMA - self._dma.active(True) + # Overwrite the last control block to be a single transfer without + # chaining, so the control block sequence properly ends. + self._cb_index -= 4 + self._add_control_block(self._cb_psram_single) - # Here is the PIO program, which is configurable to mask in the GPIO pins - # and the number of data pins. It must be configured before the state - # machine is created - @rp2.asm_pio( - in_shiftdir = rp2.PIO.SHIFT_LEFT, - push_thresh = 32, - autopush = True, - fifo_join = rp2.PIO.JOIN_RX - ) - def _pio_read_dvp(): + def _add_control_block(self, block): """ - PIO program to read DVP data from the GPIO pins. + Helper function to add a control block to the control block array. """ - wait(1, gpio, 0) # Mask in HSYNC pin - wait(1, gpio, 0) # Mask in PCLK pin - in_(pins, 1) # Mask in number of pins - wait(0, gpio, 0) # Mask in PCLK pin + # Add the control block to the array. Each control block is all 4 DMA + # alias 0 registers in order. + self._control_blocks[self._cb_index + 0] = block[0] # READ_ADDR + self._control_blocks[self._cb_index + 1] = block[1] # WRITE_ADDR + self._control_blocks[self._cb_index + 2] = block[2] # TRANS_COUNT + self._control_blocks[self._cb_index + 3] = block[3] # CTRL_TRIG + + # Increment the control block index for the next control block. + self._cb_index += 4 + + def _capture(self): + """ + Captures a frame of data from the DVP interface. + """ + # If the buffer is in SRAM and the state machine is already active, + # new frames are being captured continuously, so just return. + if not self._buffer_is_in_psram and self._sm.active() == True: + return + + # If the image buffer is in PSRAM, the streamer DMA channel needs to + # be reconfigured for each frame to reset the write address. + if self._buffer_is_in_psram: + self._dma_streamer.config( + read = addressof(self._row_buffer), # READ_ADDR + write = addressof(self._buffer), # WRITE_ADDR + count = self._bytes_per_row // 4, # TRANS_COUNT + ctrl = self._dma_ctrl_streamer, # CTRL + trigger = False, + ) + + # Configure the dispatcher DMA channel to start the control block + # sequence, but don't trigger it yet. + self._dma_dispatcher.config( + read = addressof(self._control_blocks), # READ_ADDR + write = addressof(self._dma_executer.registers), # WRITE_ADDR + count = 4, # TRANS_COUNT + ctrl = self._dma_ctrl_cb_dispatcher, # CTRL + trigger = False, + ) + + # Wait for VSYNC to go low, indicating the end of the current frame. + while Pin(self._pin_vsync, Pin.IN).value() == True: + pass + + # Activate the state machine and reset it. + self._sm.active(True) + self._sm.restart() + while self._sm.rx_fifo() > 0: + self._sm.get() + + # Start the dispatcher DMA channel. + self._dma_dispatcher.active(True) + + # If the buffer is in SRAM, the control block sequence will restart + # automatically, so nothing else to do. But if the buffer is in PSRAM, + # we need to wait for the frame to finish. + if self._buffer_is_in_psram: + # Wait for VSYNC to go high then low again, indicating the end of + # the next frame. + while Pin(self._pin_vsync, Pin.IN).value() == False: + pass + while Pin(self._pin_vsync, Pin.IN).value() == True: + pass + + # Deactivate the state machine. + self._sm.active(False) diff --git a/red_vision/cameras/hm01b0.py b/red_vision/cameras/hm01b0.py index b847a6b..83d1bb1 100644 --- a/red_vision/cameras/hm01b0.py +++ b/red_vision/cameras/hm01b0.py @@ -349,4 +349,5 @@ def read(self, image=None): - success (bool): True if the image was read, otherwise False - image (ndarray): The captured image, or None if reading failed """ + self._capture() return (True, cv2.cvtColor(self._buffer, cv2.COLOR_BayerRG2BGR, image)) diff --git a/red_vision/cameras/hm01b0_pio.py b/red_vision/cameras/hm01b0_pio.py index e07dc31..79ce867 100644 --- a/red_vision/cameras/hm01b0_pio.py +++ b/red_vision/cameras/hm01b0_pio.py @@ -50,6 +50,9 @@ def __init__( Default is 0x24 """ # Create the frame buffer + self._width = 324 + self._height = 244 + self._bytes_per_pixel = 1 self._buffer = np.zeros((244, 324), dtype=np.uint8) # Call both parent constructors @@ -63,7 +66,7 @@ def __init__( xclk_freq, sm_id, num_data_pins, - bytes_per_frame = self._buffer.size, + bytes_per_pixel = 2, byte_swap = True ) HM01B0.__init__( @@ -77,10 +80,10 @@ def open(self): """ Opens the camera and prepares it for capturing images. """ - self._active(True) + pass def release(self): """ Releases the camera and frees any resources. """ - self._active(False) + pass diff --git a/red_vision/cameras/ov5640.py b/red_vision/cameras/ov5640.py index 2539942..ecad3b8 100644 --- a/red_vision/cameras/ov5640.py +++ b/red_vision/cameras/ov5640.py @@ -903,8 +903,8 @@ def __init__( self._write_list(self._sensor_default_regs) self._colorspace = self._OV5640_COLOR_RGB - self._flip_x = False - self._flip_y = False + self._flip_x = True + self._flip_y = True self._w = None self._h = None self._size = self._OV5640_SIZE_QVGA @@ -1180,6 +1180,7 @@ def read(self, image = None): - success (bool): True if the image was read, otherwise False - image (ndarray): The captured image, or None if reading failed """ + self._capture() if self._colorspace == self._OV5640_COLOR_RGB: return (True, cv2.cvtColor(self._buffer, cv2.COLOR_BGR5652BGR, image)) elif self._colorspace == self._OV5640_COLOR_GRAYSCALE: diff --git a/red_vision/cameras/ov5640_pio.py b/red_vision/cameras/ov5640_pio.py index dd35f05..9f09e46 100644 --- a/red_vision/cameras/ov5640_pio.py +++ b/red_vision/cameras/ov5640_pio.py @@ -28,7 +28,8 @@ def __init__( pin_pclk, pin_xclk = None, xclk_freq = 5_000_000, - i2c_address = 0x3c + i2c_address = 0x3c, + buffer = None, ): """ Initializes the OV5640 PIO camera driver. @@ -47,7 +48,14 @@ def __init__( Default is 0x3c """ # Create the frame buffer - self._buffer = np.zeros((240, 320, 2), dtype=np.uint8) + if buffer is not None: + self._buffer = buffer + self._height, self._width, self._bytes_per_pixel = buffer.shape + else: + self._width = 320 + self._height = 240 + self._bytes_per_pixel = 2 + self._buffer = np.zeros((240, 320, 2), dtype=np.uint8) # Call both parent constructors DVP_RP2_PIO.__init__( @@ -60,7 +68,7 @@ def __init__( xclk_freq, sm_id, num_data_pins = 8, - bytes_per_frame = self._buffer.size, + bytes_per_pixel = 2, byte_swap = False ) OV5640.__init__( @@ -73,10 +81,10 @@ def open(self): """ Opens the camera and prepares it for capturing images. """ - self._active(True) + pass def release(self): """ Releases the camera and frees any resources. """ - self._active(False) + pass From 5b74ddcf97dbe34469b930e8d816b0d8018d2d02 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Wed, 19 Nov 2025 13:43:40 -0700 Subject: [PATCH 2/3] Change default OV5640 XCLK frequency 20MHz XCLK results in 30MHz PCLK, which the PIO can just barely handle (37.5MHz max). 25MHz XCLK results in 40MHz PCLK, which is too fast. Could also change PLL settings, but this is simpler. --- red_vision/cameras/ov5640_pio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/red_vision/cameras/ov5640_pio.py b/red_vision/cameras/ov5640_pio.py index 9f09e46..c1c8dc9 100644 --- a/red_vision/cameras/ov5640_pio.py +++ b/red_vision/cameras/ov5640_pio.py @@ -27,7 +27,7 @@ def __init__( pin_hsync, pin_pclk, pin_xclk = None, - xclk_freq = 5_000_000, + xclk_freq = 20_000_000, i2c_address = 0x3c, buffer = None, ): From 4c8b685056c56f4cfd4b5134616e4dc7aa2a925a Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Mon, 24 Nov 2025 17:54:53 -0700 Subject: [PATCH 3/3] Add optional `continuous` paramter to RP2 DVP cameras There are some edge-case issues with having the DMAs automatically capture every frame from the camera. This change makes it default to non-continuous mode, but users can enable it if they really want. --- red_vision/cameras/dvp_rp2_pio.py | 49 ++++++++++++++++++------------- red_vision/cameras/hm01b0_pio.py | 4 ++- red_vision/cameras/ov5640_pio.py | 4 ++- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/red_vision/cameras/dvp_rp2_pio.py b/red_vision/cameras/dvp_rp2_pio.py index fb779fb..530b451 100644 --- a/red_vision/cameras/dvp_rp2_pio.py +++ b/red_vision/cameras/dvp_rp2_pio.py @@ -37,7 +37,8 @@ def __init__( sm_id, num_data_pins, bytes_per_pixel, - byte_swap + byte_swap, + continuous = False ): """ Initializes the DVP interface with the specified parameters. @@ -53,6 +54,7 @@ def __init__( num_data_pins (int): Number of data pins used in DVP interface bytes_per_pixel (int): Number of bytes per pixel byte_swap (bool): Whether to swap bytes in the captured data + continuous (bool): Whether to continuously capture frames """ # Store pin assignments self._pin_d0 = pin_d0 @@ -78,6 +80,9 @@ def __init__( # Store transfer parameters self._bytes_per_pixel = bytes_per_pixel self._byte_swap = byte_swap + + # Whether to continuously capture frames + self._continuous = continuous # Set up the PIO state machine self._sm_id = sm_id @@ -363,7 +368,8 @@ def _create_control_blocks(self): num_cb = 0 if not self._buffer_is_in_psram: num_cb += 1 # PIO read control block - num_cb += 1 # Restart frame control block + if self._continuous: + num_cb += 1 # Restart frame control block else: num_cb += self._height # PIO read control blocks num_cb += self._height # Streamer control blocks @@ -405,18 +411,19 @@ def _create_control_blocks(self): # executer to write the nested `_cb_restart_frame_nested` control # block back to the dispatcher DMA registers, restarting it from the # beginning of the control block sequence. - self._cb_restart_frame_nested = array.array('I', [ - addressof(self._control_blocks), # READ_ADDR - addressof(self._dma_executer.registers), # WRITE_ADDR - 4, # TRANS_COUNT - self._dma_ctrl_cb_dispatcher, # CTRL_TRIG - ]) - self._cb_restart_frame = array.array('I', [ - addressof(self._cb_restart_frame_nested), # READ_ADDR - addressof(self._dma_dispatcher.registers), # WRITE_ADDR - len(self._cb_restart_frame_nested), # TRANS_COUNT - self._dma_ctrl_cb_executer_nested_single, # CTRL_TRIG - ]) + if self._continuous: + self._cb_restart_frame_nested = array.array('I', [ + addressof(self._control_blocks), # READ_ADDR + addressof(self._dma_executer.registers), # WRITE_ADDR + 4, # TRANS_COUNT + self._dma_ctrl_cb_dispatcher, # CTRL_TRIG + ]) + self._cb_restart_frame = array.array('I', [ + addressof(self._cb_restart_frame_nested), # READ_ADDR + addressof(self._dma_dispatcher.registers), # WRITE_ADDR + len(self._cb_restart_frame_nested), # TRANS_COUNT + self._dma_ctrl_cb_executer_nested_single, # CTRL_TRIG + ]) else: # Control block for executer to read 1 row from PIO RX FIFO to image # buffer. @@ -468,8 +475,10 @@ def _assemble_control_blocks(self): # FIFO to image buffer. self._add_control_block(self._cb_pio_repeat) - # Add control block to restart by reconfiguring the dispatcher DMA. - self._add_control_block(self._cb_restart_frame) + # If continuous mode is requested, add control block to restart the + # control block sequence reconfiguring the dispatcher DMA. + if self._continuous: + self._add_control_block(self._cb_restart_frame) else: # Loop through each row of the image. for row in range(self._height): @@ -542,10 +551,10 @@ def _capture(self): # Start the dispatcher DMA channel. self._dma_dispatcher.active(True) - # If the buffer is in SRAM, the control block sequence will restart - # automatically, so nothing else to do. But if the buffer is in PSRAM, - # we need to wait for the frame to finish. - if self._buffer_is_in_psram: + # If the buffer is in SRAM and we're in continuous mode, the control + # block sequence will restart automatically, so nothing else to do. But + # if the buffer is in PSRAM, we need to wait for the frame to finish. + if self._buffer_is_in_psram or self._continuous == False: # Wait for VSYNC to go high then low again, indicating the end of # the next frame. while Pin(self._pin_vsync, Pin.IN).value() == False: diff --git a/red_vision/cameras/hm01b0_pio.py b/red_vision/cameras/hm01b0_pio.py index 79ce867..8135dc2 100644 --- a/red_vision/cameras/hm01b0_pio.py +++ b/red_vision/cameras/hm01b0_pio.py @@ -30,6 +30,7 @@ def __init__( xclk_freq = 25_000_000, num_data_pins = 1, i2c_address = 0x24, + continuous = False, ): """ Initializes the HM01B0 PIO camera driver. @@ -67,7 +68,8 @@ def __init__( sm_id, num_data_pins, bytes_per_pixel = 2, - byte_swap = True + byte_swap = True, + continuous = continuous, ) HM01B0.__init__( self, diff --git a/red_vision/cameras/ov5640_pio.py b/red_vision/cameras/ov5640_pio.py index c1c8dc9..c03f266 100644 --- a/red_vision/cameras/ov5640_pio.py +++ b/red_vision/cameras/ov5640_pio.py @@ -30,6 +30,7 @@ def __init__( xclk_freq = 20_000_000, i2c_address = 0x3c, buffer = None, + continuous = False, ): """ Initializes the OV5640 PIO camera driver. @@ -69,7 +70,8 @@ def __init__( sm_id, num_data_pins = 8, bytes_per_pixel = 2, - byte_swap = False + byte_swap = False, + continuous = continuous, ) OV5640.__init__( self,