diff --git a/.gitmodules b/.gitmodules index cb79b01..e9033f6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "firmware/lib/rp2040.pwm"] path = firmware/lib/rp2040.pwm url = git@github.com:AllenNeuralDynamics/rp2040.pwm.git +[submodule "firmware/lib/etl"] + path = firmware/lib/etl + url = https://github.com/ETLCPP/etl.git diff --git a/device.yml b/device.yml index b93c2c4..ce6e917 100644 --- a/device.yml +++ b/device.yml @@ -1,7 +1,277 @@ %YAML 1.1 --- # yaml-language-server: $schema=https://harp-tech.org/draft-02/schema/device.json -device: ValveController -whoAmI: 1406 -firmwareVersion: "0.0.0" -hardwareTargets: "1.0.0" +device: DelphiController +whoAmI: 1409 +firmwareVersion: "0.0" +hardwareTargets: "1.0" +registers: + ValveState: + address: 32 + type: U16 + access: Event + description: "Set the enabled/disabled state (enabled = 1) of all valves" + ValvesSet: + address: 33 + type: U16 + maskType: ValveMask + access: Write + description: "Write a 1 to any bit to enable the corresponding valve." + ValvesClear: + address: 34 + type: U16 + maskType: ValveMask + access: Write + description: "Write a 1 to any bit to disable the corresponding valve." + ValveConfig0: &valveConfig + address: 35 + type: U8 + length: 12 + access: Write + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve0." + ValveConfig1: + <<: *valveConfig + address: 36 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve1." + ValveConfig2: + <<: *valveConfig + address: 37 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve2." + ValveConfig3: + <<: *valveConfig + address: 38 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve3." + ValveConfig4: + <<: *valveConfig + address: 39 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve4." + ValveConfig5: + <<: *valveConfig + address: 40 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve5." + ValveConfig6: + <<: *valveConfig + address: 41 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve6." + ValveConfig7: + <<: *valveConfig + address: 42 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve7." + ValveConfig8: + <<: *valveConfig + address: 43 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve8." + ValveConfig9: + <<: *valveConfig + address: 44 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve9." + ValveConfig10: + <<: *valveConfig + address: 45 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve10." + ValveConfig11: + <<: *valveConfig + address: 46 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve11." + ValveConfig12: + <<: *valveConfig + address: 47 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve12." + ValveConfig13: + <<: *valveConfig + address: 48 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve13." + ValveConfig14: + <<: *valveConfig + address: 49 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve14." + ValveConfig15: + <<: *valveConfig + address: 50 + description: "the hit duty cycle (float: 0 - 1.0), hold duty cycle (float: 0 - 1.0), and hit duration in microseconds (U32) for Valve15." + AuxGPIODir: + address: 51 + type: U8 + maskType: AuxGPIOMask + access: Write + description: "Specify each auxiliary GPIO pin as an input (0) or output (1)." + AuxGPIOState: + address: 52 + type: U8 + maskType: AuxGPIOMask + access: Write + description: "Set the state (on or off) of any auxiliary GPIO pins specified as outputs." + AuxGPIOSet: + address: 53 + type: U8 + maskType: AuxGPIOMask + access: Write + description: "When writing a 1 to any bit, turn on the specified auxiliary GPIO pins specified as outputs." + AuxGPIOClear: + address: 54 + type: U8 + maskType: AuxGPIOMask + access: Write + description: "When writing a 1 to any bit, Turn off the specified auxiliary GPIO pins specified as outputs." + AuxGPIOInputRiseEvent: + address: 55 + type: U8 + maskType: AuxGPIOMask + access: Event + description: "" + AuxGPIOInputFallEvent: + address: 56 + type: U8 + maskType: AuxGPIOMask + access: Event + description: "" + AuxGPIORisingInputs: + address: 57 + type: U8 + maskType: AuxGPIOMask + access: Write + description: "" + AuxGPIOFallingInputs: + address: 58 + type: U8 + maskType: AuxGPIOMask + access: Write + description: "" + PokePin: + address: 59 + type: U8 + access: Write + description: "which poke ports are active." + PokePinInverted: + address: 60 + type: U8 + access: Write + description: "Which poke ports are inverted (i.e: transition from HIGH to LOW when a poke occurs)." + PokeState: + address: 61 + type: U8 + access: Event + description: "The state of the poke port. An event will be triggered given a poke/ beam break that is greater than the min poke time." + RawPokeState: + address: 62 + type: U8 + access: Event + description: "The raw state of the poke pin. Events will be triggered at the onset of a beam break (1) and offset (0)." + PokeDometer: + address: 63 + type: U32 + access: Read + description: "number of mouse pokes per port since boot or reset." + FSMState: + address: 64 + type: U8 + access: Write + description: "Enable (1) (aka reset) or Disable (0) the poke handling state machine. Note that QueuedOdorIndex must be specified first. Disabling and then enabling a previously-enabled FSM will reset it to its starting state." + ForceFSM: + address: 65 + type: U8 + access: Write + description: "Force the poke handling state machine to iterate as if handling a mouse poke. PokeDometers are not incremented." + QueuedOdorMask: + address: 66 + type: U16 + access: Event + description: "Queued odors (value: odor valve mask) that will be delivered to the odor port given a register poke. After odors have been dispensed, the register will be set to 0, which indicates that new odors are needed" + VacuumCloseTimeUS: + address: 67 + type: U32 + access: Write + description: "Time alotted (in microseconds) for the vacuum valve to close." + MinOdorDeliveryTimeUS: + address: 68 + type: U32 + access: Write + description: "Minimum time alotted (in microseconds) for the odor delivery state." + MaxOdorDeliveryTimeUS: + address: 69 + type: U32 + access: Write + description: "Maximum time alotted (in microseconds) for the odor delivery state." + OdorTransitionTimeUS: + address: 70 + type: U32 + access: Write + description: "Time alotted (in microseconds) before the vacuum turns on to remove the current odor." + VacuumSetupTimeUS: + address: 71 + type: U32 + access: Write + description: "Time alotted (in microseconds) for the vacuum to open." + FinalValveEnergizedTimeUS: + address: 72 + type: U32 + access: Write + description: "Time alotted (in microseconds) for the final valve to open and remain on." + MinimumPokeTimeUS: + address: 73 + type: U32 + access: Write + description: "Minimum time (in microseconds) necessary for a mouse poke port beam to be broken before being interpretted as a poke." + CamPin: + address: 74 + type: U8 + access: Write + description: "The GPIO output pin used for camera triggering. Default pin is 26." + CamPinState: + address: 75 + type: U8 + access: Event + description: "Event is initiated when a rising edge of the camera triggered signal (PWM) is detected. The actual value of the pin doesn't change." + FrameRate: + address: 76 + type: U32 + access: Write + description: "Set the frame rate of the camera trigger/ frequency of the PWM signal." + DutyCycle: + address: 77 + type: Float + access: Write + description: "Set the duty cycle of the PWM. Default and recommend is 0.5 for producing a square wave." + EnableCamTrigger: + address: 78 + type: U8 + access: Write + description: "Enable (1) and disable (0) camera triggering/ the PWM signal." + EnableValveLeds: + address: 79 + type: U8 + access: Write + description: "Enable (1) and disable (0) valve LEDs." + +groupMasks: + ValveMask: + description: "Valve that can be configured/enabled/disabled" + values: + Valve0: 0x0 + Valve1: 0x1 + Valve2: 0x2 + Valve3: 0x3 + Valve4: 0x4 + Valve5: 0x5 + Valve6: 0x6 + Valve7: 0x7 + Valve8: 0x8 + Valve9: 0x9 + Valve10: 0xA + Valve11: 0xB + Valve12: 0xC + Valve13: 0xD + Valve14: 0xE + Valve15: 0xF + AllValves: 0xFFFF + AuxGPIOMask: + description: "Auxiliary GPIO index." + values: + AuxGPIO0: 0x01 + AuxGPIO1: 0x02 + AuxGPIO2: 0x04 + AuxGPIO3: 0x08 + AuxGPIO4: 0x10 + AuxGPIO5: 0x20 + AuxGPIO6: 0x40 + AuxGPIO7: 0x80 \ No newline at end of file diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index 7df4c24..c02e046 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -1,3 +1,19 @@ +# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work == +if(WIN32) + set(USERHOME $ENV{USERPROFILE}) +else() + set(USERHOME $ENV{HOME}) +endif() +set(sdkVersion 2.2.0) +set(toolchainVersion 14_2_Rel1) +set(picotoolVersion 2.2.0) +set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake) +if (EXISTS ${picoVscode}) + include(${picoVscode}) +endif() +# ==================================================================================== +set(PICO_BOARD pico CACHE STRING "Board type") + cmake_minimum_required(VERSION 3.13) find_package(Git REQUIRED) execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --short HEAD OUTPUT_VARIABLE COMMIT_ID OUTPUT_STRIP_TRAILING_WHITESPACE) @@ -6,42 +22,56 @@ add_definitions(-DGIT_HASH="${COMMIT_ID}") # Usable in source code. #add_definitions(-DDEBUG) # Uncomment for debugging add_definitions(-DUSBD_MANUFACTURER="Allen Institute") -add_definitions(-DUSBD_PRODUCT="valve-controller") +add_definitions(-DUSBD_PRODUCT="delphi-controller") # PICO_SDK_PATH must be defined. include(${PICO_SDK_PATH}/pico_sdk_init.cmake) # Use modern conventions like std::invoke -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 23) -project(valve-controller) +project(delphi-controller) pico_sdk_init() add_subdirectory(lib/harp.core.rp2040/firmware) # Path to harp.core.rp2040. add_subdirectory(lib/rp2040.pwm) +add_subdirectory(lib/etl build/etl) # etl library path add_executable(${PROJECT_NAME} src/main.cpp - src/valve_controller_app.cpp + src/delphi_controller_app.cpp ) add_library(valve_driver src/valve_driver.cpp ) +add_library(poke_manager + src/poke_manager.cpp +) + +add_library(pwm_pio + src/pwm_pio.cpp +) + include_directories(inc) target_link_libraries(valve_driver rp2040_pwm pico_stdlib) -target_link_libraries(${PROJECT_NAME} harp_core harp_c_app rp2040_pwm - valve_driver harp_sync pico_stdlib) - -pico_add_extra_outputs(${PROJECT_NAME}) +target_link_libraries(poke_manager pico_stdlib valve_driver etl::etl) +target_link_libraries(pwm_pio hardware_pio pico_stdlib) +target_link_libraries(${PROJECT_NAME} + harp_core harp_c_app rp2040_pwm poke_manager hardware_pio hardware_gpio pwm_pio valve_driver harp_sync + pico_stdlib etl::etl) #pico_stdio_usb if(DEBUG) message(WARNING "Debug printf() messages from harp core to UART with baud \ rate 921600.") pico_enable_stdio_uart(${PROJECT_NAME} 1) # UART stdio for printf. + pico_enable_stdio_uart(poke_manager 1) + pico_enable_stdio_uart(pwm_pio 1) # Additional libraries need to have stdio init also. endif() +pico_add_extra_outputs(${PROJECT_NAME}) + diff --git a/firmware/inc/config.h b/firmware/inc/config.h index 9f31f8d..fffa0f0 100644 --- a/firmware/inc/config.h +++ b/firmware/inc/config.h @@ -2,18 +2,23 @@ #define CONFIG_H #define NUM_VALVES (16) +#define NUM_ODOR_VALVES (14) +#define FINAL_VALVE_INDEX (0) +#define VACCUM_VALVE_INDEX (1) +#define CAM_TRIGGER_PIN (26) +#define LED_ENABLE_PIN (4) #define UART_TX_PIN (0) #define HARP_SYNC_RX_PIN (5) #define HARP_CORE_LED_PIN (2) -#define VALVE_PIN_BASE (6) -#define GPIO_PIN_BASE (22) +inline constexpr uint32_t VALVE_PIN_BASE = 6; +inline constexpr uint32_t GPIO_PIN_BASE = 22; #define VALVES_MASK (0x0000FFFF) #define GPIOS_MASK (0x000000FF) -#define HARP_DEVICE_ID (1406) +#define HARP_DEVICE_ID (1409) #define HW_VERSION_MAJOR (1) #define HW_VERSION_MINOR (0) #define HW_ASSEMBLY_VERSION (0) diff --git a/firmware/inc/delphi_controller_app.h b/firmware/inc/delphi_controller_app.h new file mode 100644 index 0000000..b5dd6e5 --- /dev/null +++ b/firmware/inc/delphi_controller_app.h @@ -0,0 +1,225 @@ +#ifndef DELPHI_CONTROLLER_APP_H +#define DELPHI_CONTROLLER_APP_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DEBUG + #include + #include // for printf +#endif + +// Setup for Harp App +inline constexpr size_t APP_REG_COUNT = 48; +// Numeric addresses for Harp Registers (clunky) -- DO ALL NEW REGISTERS NEED TO BE REFERENCED TO THESE?? +inline constexpr size_t VALVE_START_APP_ADDRESS = APP_REG_START_ADDRESS + 3; +inline constexpr size_t LAST_VALVE_APP_ADDRESS = VALVE_START_APP_ADDRESS + NUM_VALVES - 1; +inline constexpr size_t AUX_GPIO_INPUT_RISE_EVENT_ADDRESS = LAST_VALVE_APP_ADDRESS + 5; +inline constexpr size_t AUX_GPIO_RISING_INPUTS_ADDRESS = AUX_GPIO_INPUT_RISE_EVENT_ADDRESS + 2; +inline constexpr size_t AUX_GPIO_FALLING_INPUTS_ADDRESS = AUX_GPIO_INPUT_RISE_EVENT_ADDRESS + 3; + +extern RegSpecs app_reg_specs[APP_REG_COUNT]; +extern RegFnPair reg_handler_fns[APP_REG_COUNT]; +extern HarpCApp& app; + +extern ValveDriver valve_drivers[NUM_VALVES]; +extern PokeManager poke_manager; +extern CameraDriver cam_driver; + +extern uint8_t old_aux_gpio_inputs; + +// struct for HARP event queueing +static inline constexpr uint8_t CAM_PIN_STATE_INDEX_ADDRESS = 75; +struct HarpEvent { + uint8_t index; + uint64_t timestamp; +}; + +//Valves state mask variables +const uint8_t VALVES_STATE_INDEX_ADDRESS = 32; + +// Valve configuration struct for configuring the Hit-and-hold driver +#pragma pack(push, 1) +struct ValveConfig +{ + float hit_output; + float hold_output; + uint32_t hit_duration_us; +}; +#pragma pack(pop) + +// Registers +#pragma pack(push, 1) +struct app_regs_t +{ + uint16_t ValvesState; // Raw (energized/deenergized) state of all valves. + // Bitmask: one bit per valve. + // Write: 0 = deenergize. 1 = energize + // Read: 0 = deenergized. 1 = energized + uint16_t ValvesSet; // Energize the valves specified in the bitmask. + // Bitmask: one bit per valve. (1 = energize) + // Read values are identical to ValveStates. + uint16_t ValvesClear; // Deenergize the valve specified in the bitmask. + // Bitmask: one bit per valve. (1 = de-energize) + // Read values are the bitwise inverse of ValvesState + /// @ref ValveConfigs are represented as 16 individual registers instead + /// of a register with an array of 16 ValveConfigs. + ValveConfig ValveConfigs[NUM_VALVES]; // Represents app regs: 35, 36, ... 50 + // 16 Heterogeneous registers each + // representing a ValveConfig packed + // struct. + uint8_t AuxGPIODir; + uint8_t AuxGPIOState; + uint8_t AuxGPIOSet; + uint8_t AuxGPIOClear; + + uint8_t AuxGPIOInputRiseEvent; + uint8_t AuxGPIOInputFallEvent; + uint8_t AuxGPIORisingInputs; // Raw state of which inputs rose (could be multiple) + uint8_t AuxGPIOFallingInputs; // Raw state of which inputs fell (could be multiple) + + // Poke Manager app "registers" here. + uint8_t PokePin; + uint8_t PokePinInverted; + uint8_t PokeState; + uint8_t RawPokeState; + uint32_t PokeDometer; + uint8_t FSMEnabledState; + uint8_t ForceFSM; + int16_t QueuedOdorMask; + uint32_t VacuumCloseTimeUS; + uint32_t MinOdorDeliveryTimeUS; + uint32_t MaxOdorDeliveryTimeUS; + uint32_t OdorTransitionTimeUS; + uint32_t VacuumSetupTimeUS; + uint32_t FinalValveEnergizedTimeUS; + uint32_t MinimumPokeTimeUS; + uint8_t CamPin; + uint8_t CamPinState; + uint32_t FrameRate; + float DutyCycle; + uint8_t EnableCamTrigger; + uint8_t EnableValveLeds; +}; +#pragma pack(pop) + +extern app_regs_t app_regs; + +/** + * \brief callback function to tell the PC we need another odor from + * within the PokeManager state machine logic. + */ +void request_next_odor(void); + +/** + * \brief callback function to tell the PC when the poke state changed + */ +void poke_state_changed(void); + +/** + * \brief callback function to tell the PC when the beam broke (raw poke) + */ +void raw_poke_rise(void); + +/** + * \brief callback function to tell the PC when the beam broke (raw poke) + */ +void raw_poke_fall(void); + +/** + * \brief callback for camera timestamp + */ +void camera_timestamp_callback(uint gpio, uint32_t events); + + +/** + * \brief function for queueing HARP events + */ +void push_event_from_isr(uint8_t index, uint64_t timestamp); +bool pop_event(HarpEvent &event); + +/** + * \brief function getting valve state mask + */ +uint16_t get_valve_mask(); + + +/** + * \brief update the app state. Called in a loop. + */ +void update_app_state(); + +/** + * \brief reset the app. + */ +void reset_app(); + +inline uint8_t read_aux_gpios() +{return uint8_t((gpio_get_all() >> GPIO_PIN_BASE) & GPIOS_MASK);} + +void read_valves_state(uint8_t reg_address); +void read_valves_set(uint8_t reg_address); +void read_valves_clear(uint8_t reg_address); +void read_any_valve_config(uint8_t reg_address); +void read_aux_gpio_state(uint8_t reg_address); + +void read_poke_pin(uint8_t reg_address); +void read_poke_pin_inverted(uint8_t reg_address); +void read_poke_state(uint8_t reg_address); +void read_raw_poke_state(uint8_t reg_address); +void read_pokedometer(uint8_t reg_address); +void read_fsm_enabled_state(uint8_t reg_address); +//void read_force_fsm(uint8_t reg_address); // aliased to read_reg_generic +void read_current_odors(uint8_t reg_address); +void read_vacuum_close_time_us(uint8_t reg_address); +void read_min_odor_delivery_time_us(uint8_t reg_address); +void read_max_odor_delivery_time_us(uint8_t reg_address); +void read_odor_transition_time_us(uint8_t reg_address); +void read_vacuum_setup_time_us(uint8_t reg_address); +void read_final_valve_energized_time_us(uint8_t reg_address); +void read_minimum_poke_time_us(uint8_t reg_address); + +void read_cam_pin(uint8_t reg_address); +void read_cam_pin_state(uint8_t reg_address); +void read_frame_rate(uint8_t reg_address); +void read_duty_cycle(uint8_t reg_address); +void read_enable_cam_trigger(uint8_t reg_address); +void read_valve_leds(uint8_t reg_address); + +void write_valves_state(msg_t& msg); +void write_valves_set(msg_t& msg); +void write_valves_clear(msg_t& msg); +void write_any_valve_config(msg_t& msg); +void write_aux_gpio_dir(msg_t& msg); +void write_aux_gpio_state(msg_t& msg); +void write_aux_gpio_set(msg_t& msg); +void write_aux_gpio_clear(msg_t& msg); + +void write_poke_pin(msg_t& msg); +void write_poke_pin_inverted(msg_t& msg); +// Cannot write to poke_stage +// Cannot write to pokedometer +void write_fsm_enabled_state(msg_t& msg); +void write_force_fsm(msg_t& msg); +void write_current_odors(msg_t& msg); +void write_vacuum_close_time_us(msg_t& msg); +void write_min_odor_delivery_time_us(msg_t& msg); +void write_max_odor_delivery_time_us(msg_t& msg); +void write_odor_transition_time_us(msg_t& msg); +void write_vacuum_setup_time_us(msg_t& msg); +void write_final_valve_energized_time_us(msg_t& msg); +void write_minimum_poke_time_us(msg_t& msg); + +void write_cam_pin(msg_t& msg); +void write_frame_rate(msg_t& msg); +void write_duty_cycle(msg_t& msg); +void write_enable_cam_trigger(msg_t& msg); +void write_valve_leds(msg_t& msg); + +#endif // DELPHI_CONTROLLER_APP_H diff --git a/firmware/inc/poke_manager.h b/firmware/inc/poke_manager.h new file mode 100644 index 0000000..8d114c7 --- /dev/null +++ b/firmware/inc/poke_manager.h @@ -0,0 +1,306 @@ +#ifndef POKE_MANAGER_H // Include Gaurd +#define POKE_MANAGER_H + +#include +#include +#include // for uart printing +#include // for printf +#include +#include +#include + + +class PokeManager +{ +public: + + enum state_t + { + RESET, + ODOR_SETUP, + ODOR_DISPENSING_TO_EXHAUST, + ODOR_DELIVERY_TO_FINAL_VALVE, + ODOR_PRECLEAN, + VAC_START, + ODOR_PURGE, + }; + + + // Declare constructor + PokeManager( + ValveDriver& final_valve, //Pass by reference (work of this org. object) + ValveDriver& vac_valve, + ValveDriver (&odor_valves)[], + size_t num_odor_valves + ); + + ~PokeManager(); // desctructor + + void update(); + + void reset(); // reset the fsm + + inline void enable() + {set_enabled_state(true);} + + inline void disable() + {set_enabled_state(false);} + + // Generate an ETL vector of which odor valves are queued + inline etl::vector bit_positions(uint16_t mask) { + etl::vector positions; + for (int i = 0; i < 16; ++i) if (mask & (1 << i)) positions.push_back(i); + return positions; + } + + inline void set_odor_valve_state(bool enabled) + { + auto odor_valve_indices = bit_positions(odor_valve_mask_); // get positions valves to activate + for (int odor_valve_index: odor_valve_indices){ + if (odor_valve_index < 14) // Only 14 odor valves -- any odor valve specified above this is ignored + { + if (enabled) + odor_valves_[odor_valve_index].energize(); + else + odor_valves_[odor_valve_index].deenergize(); + } + } + } + + inline void energize_odor_valve() + {set_odor_valve_state(1); + valve_state_ = true; + } + + inline void deenergize_odor_valve() + {set_odor_valve_state(0); + valve_state_ = false; + } + + // Event Handlers + inline void set_next_odor_callback_fn( void (* fn)(void)) + {request_next_odor_callback_fn_ = fn;} + + inline void request_next_odor() + { + if (request_next_odor_callback_fn_ != nullptr) + request_next_odor_callback_fn_(); + } + + inline void set_poke_state_callback_fn( void (* fn)(void)) + {request_poke_state_callback_fn_ = fn;} + + inline void poke_state_changed() + { + if (request_poke_state_callback_fn_ != nullptr) + request_poke_state_callback_fn_(); + } + + // Rise and Fall poke events + inline void set_raw_poke_rise_callback_fn( void (* fn)(void)) + {request_raw_poke_rise_callback_fn_ = fn;} + + inline void raw_poke_rise() + { + if (request_raw_poke_rise_callback_fn_ != nullptr) + request_raw_poke_rise_callback_fn_(); + } + + inline void set_raw_poke_fall_callback_fn( void (* fn)(void)) + {request_raw_poke_fall_callback_fn_ = fn;} + + inline void raw_poke_fall() + { + if (request_raw_poke_fall_callback_fn_ != nullptr) + request_raw_poke_fall_callback_fn_(); + } + +/* + * \brief enable (true) or disable (false) the odor delivery state machine. + */ + void set_enabled_state(bool enabled); + + inline void set_current_odors(uint16_t odor_mask) + {odor_valve_mask_ = odor_mask;} + + inline void set_vacuum_close_time_us(uint32_t vacuum_close_time_us) + {vacuum_close_time_us_ = vacuum_close_time_us;} + + inline void set_min_odor_delivery_time_us(uint32_t min_odor_delivery_time_us) + {min_odor_delivery_time_us_ = min_odor_delivery_time_us;} + + inline void set_max_odor_delivery_time_us(uint32_t max_odor_delivery_time_us) + {max_odor_delivery_time_us_ = max_odor_delivery_time_us;} + + void set_odor_transition_time_us(uint32_t odor_transition_time_us) + {odor_transition_time_us_ = odor_transition_time_us;} + + inline void set_vacuum_setup_time_us(uint32_t vac_setup_time_us) + {vac_setup_time_us_ = vac_setup_time_us;} + + inline void set_final_valve_energized_time_us(uint32_t final_valve_energized_time_us) + {final_valve_energized_time_us_ = final_valve_energized_time_us;} + + inline void set_min_poke_time_us(uint32_t min_poke_time_us) + {min_poke_time_us_ = min_poke_time_us;} + + inline void set_poke_pin(uint8_t pin) + { + clear_poke_pin(); + // Init new gpio pin. + poke_pin_ = pin; + gpio_init(poke_pin_); + gpio_set_dir(poke_pin_, GPIO_IN); + poke_pin_is_initialized_ = true; + // Apply override state. + set_poke_pin_override_state(override_state_); + } + + inline void clear_poke_pin() + { + if (!poke_pin_is_initialized_) + return; + set_poke_pin_override_state(GPIO_OVERRIDE_NORMAL); + gpio_deinit(poke_pin_); + poke_pin_ = DEFAUT_POKE_PIN; + poke_pin_is_initialized_ = false; + } + + inline void force_poke() + {poke();} + +/** + * \brief set the poke pin input override state to (1) invert or (0) uninvert + * the input. +*/ + inline void set_poke_pin_override_state(gpio_override override_state) + { + override_state_ = override_state; // Cache the override setting. + if (poke_pin_is_initialized_) + gpio_set_inover(poke_pin_, override_state); + } + + void deenergize_all_valves(); + +/** + * \brief true if the poke pin is inverted. +*/ + inline uint8_t poke_pin_is_inverted() const + { + gpio_override override_state = + gpio_override( + (io_bank0_hw->io[poke_pin_].ctrl & IO_BANK0_GPIO0_CTRL_INOVER_BITS) + >> IO_BANK0_GPIO0_CTRL_INOVER_LSB); + return override_state == GPIO_OVERRIDE_INVERT; + } + + inline uint32_t get_enabled_state() const + {return !disable_fsm_;} + + inline uint8_t get_poke_pin() const + {return poke_pin_;} + + inline uint8_t get_poke_state() const + {return poke_state_;} + + inline uint8_t get_raw_poke_state() const + {return raw_poke_state_;} + + inline size_t get_poke_count() const + {return poke_count_;} + + inline uint16_t get_current_odors() const + {return odor_valve_mask_;} + + inline uint32_t get_vacuum_close_time_us() const + {return vacuum_close_time_us_;} + + inline uint32_t get_min_odor_delivery_time_us() const + {return min_odor_delivery_time_us_;} + + inline uint32_t get_max_odor_delivery_time_us() const + {return max_odor_delivery_time_us_;} + + inline uint32_t get_odor_transition_time_us() const + {return odor_transition_time_us_;} + + inline uint32_t get_vacuum_setup_time_us() const + {return vac_setup_time_us_;} + + inline uint32_t get_final_valve_energized_time_us() const + {return final_valve_energized_time_us_;} + + inline uint32_t get_min_poke_time_us() const + {return min_poke_time_us_;} + +private: + +/** + * \brief update whether or not the input has seen a poke. Called in a loop. + */ + void update_poke_status(); + +/** + * \brief count a poke. + */ + inline void poke() + {poke_detected_ = true;} + + +/** + * \brief time we've been in the current state. + */ + inline uint32_t state_duration_us() + {return time_us_32() - state_entry_time_us_;} + + // Declare data members + state_t state_; + uint32_t state_entry_time_us_; + uint32_t poke_start_time_us_; + + uint8_t poke_pin_; + gpio_override override_state_; /// Whether or not the poke pin is inverted. + + uint16_t odor_valve_mask_; + + size_t poke_count_; + uint8_t poke_state_; + uint8_t raw_poke_state_; + bool valve_state_; + bool poke_detected_; + bool disable_fsm_; + bool beam_broken_; //keep track of beam state + bool poke_initiated_once_; //Only trigger the FSM on 1 poke + ValveDriver& vac_valve_; + ValveDriver& final_valve_; + + ValveDriver (&odor_valves_)[]; + size_t num_odor_valves_; + + uint32_t vacuum_close_time_us_; + uint32_t min_odor_delivery_time_us_; + uint32_t max_odor_delivery_time_us_; + uint32_t odor_transition_time_us_; + uint32_t vac_setup_time_us_; + uint32_t final_valve_energized_time_us_; + uint32_t min_poke_time_us_; + + void (*request_next_odor_callback_fn_)(void); + void (*request_poke_state_callback_fn_)(void); + void (*request_raw_poke_rise_callback_fn_)(void); + void (*request_raw_poke_fall_callback_fn_)(void); + + bool poke_pin_is_initialized_; + + // Declare Constants + static inline constexpr uint32_t DEFAULT_VACUUM_CLOSE_TIME_US = 20e3; + static inline constexpr uint32_t DEFAULT_MIN_ODOR_DELIVERY_TIME_US = 10e3; + static inline constexpr uint32_t DEFAULT_MAX_ODOR_DELIVERY_TIME_US = 10e6; + static inline constexpr uint32_t DEFAULT_ODOR_TRANSITION_TIME_US = 30e3; + static inline constexpr uint32_t DEFAULT_VACUUM_SETUP_TIME_US = 20e3; + static inline constexpr uint32_t DEFAULT_FINAL_VALVE_ENERGIZED_TIME_US = 110e3; + static inline constexpr uint32_t MIN_POKE_TIME_US = 10e3; + static inline constexpr uint8_t DEFAUT_POKE_PIN = GPIO_PIN_BASE; +}; + +#endif // POKE_MANAGER_H diff --git a/firmware/inc/poke_manager_test.h b/firmware/inc/poke_manager_test.h new file mode 100644 index 0000000..f054d4f --- /dev/null +++ b/firmware/inc/poke_manager_test.h @@ -0,0 +1,75 @@ +#ifndef POKE_MANAGER_H // Include Gaurd +#define POKE_MANAGER_H + +#include +#include // for uart printing +#include // for printf +#include +#include +#include + + +class PokeManager +{ +public: + + enum state_t + { + RESET, + ODOR_SETUP, + ODOR_DISPENSING_TO_EXHAUST, + ODOR_DELIVERY_TO_FINAL_VALVE, + ODOR_PRECLEAN, + VAC_START, + ODOR_PURGE, + }; + + + // Declare constructor + PokeManager( + ValveDriver& final_valve, //Pass by reference (work of this org. object) + ValveDriver& vac_valve, + etl::vector& odor_valves + ); + + ~PokeManager(); // desctructor + + void update(); + +/** + * \brief true if a poke was detected. Inline replaces function with code + */ + inline void poke() + {poke_detected_ = true;} + + void deenergize_all_valves(); + +private: + +/** + * \brief time we've been in the current state. + */ + inline uint32_t state_duration_us() + {return time_us_32() - state_entry_time_us_;} + + // Declare data members + state_t state_; + uint32_t state_entry_time_us_; + + int valve_index_; + size_t poke_count_; + bool poke_detected_; + ValveDriver& vac_valve_; + ValveDriver& final_valve_; + etl::vector& odor_valves_; + + // Declare Constants + static inline constexpr uint32_t VACUUM_CLOSE_TIME_US = 20e3; + static inline constexpr uint32_t ODOR_DELIVERY_TIME_US = 10e3; + static inline constexpr uint32_t ODOR_TRANSITION_TIME_US = 30e3; + static inline constexpr uint32_t VAC_SETUP_TIME_US = 20e3; + static inline constexpr uint32_t FINAL_VALVE_ENERGIZED_TIME_US = 110e3; + +}; + +#endif // POKE_MANAGER_H diff --git a/firmware/inc/pwm_pio.h b/firmware/inc/pwm_pio.h new file mode 100644 index 0000000..f22e15d --- /dev/null +++ b/firmware/inc/pwm_pio.h @@ -0,0 +1,195 @@ +// ---------------------------------------------------------------- // +// This file is autogenerated by pioasm version 2.2.0; do not edit! // +// ---------------------------------------------------------------- // + +#pragma once + +#include +#include +#include +#include + +// --- // +// pwm // +// --- // + +#define pwm_wrap_target 0 +#define pwm_wrap 8 +#define pwm_pio_version 0 + +/** + * \brief Configure PIO for PWM + */ +static const uint16_t pwm_program_instructions[] = { + // .wrap_target + 0x80a0, // 0: pull block + 0xa027, // 1: mov x, osr + 0x80a0, // 2: pull block + 0xa047, // 3: mov y, osr + 0xe001, // 4: set pins, 1 + 0x0045, // 5: jmp x--, 5 + 0xe000, // 6: set pins, 0 + 0x0087, // 7: jmp y--, 7 + 0x0000, // 8: jmp 0 + // .wrap +}; + +#if !PICO_NO_HARDWARE +static const struct pio_program pwm_program = { + .instructions = pwm_program_instructions, + .length = 9, + .origin = -1, + .pio_version = pwm_pio_version, +#if PICO_PIO_VERSION > 0 + .used_gpio_ranges = 0x0 +#endif +}; + +static inline pio_sm_config pwm_program_get_default_config(uint offset) { + pio_sm_config c = pio_get_default_sm_config(); + sm_config_set_wrap(&c, offset + pwm_wrap_target, offset + pwm_wrap); + return c; +} +#endif + +class CameraDriver +{ +public: + +/** + * \brief constructor. + * \param pwm_pio_pin the pwm output pin to the controller enable pin. + */ + CameraDriver(uint8_t pwm_pio_pin); + +/** + * \brief destructor. + */ + ~CameraDriver(); + +/** + * \brief reset to PIO. + * \details PWM freqency will be set to zero until fps is specified + */ + void reset(); + +/** + * \brief Call periodically in a loop to update the internal finite state + * machine that controls the pwm. + */ + void update(); + +/** + * \brief Initialize PWM on PIO + */ + void pwm_init(PIO pio, uint sm, uint offset, uint8_t pin, uint8_t enable_state); + +/** + * \brief Initialize edge detection + */ + void edge_detection_init(PIO pio, uint sm_edge, uint8_t pin, uint8_t enable_state); + +/** + * \brief Edge detection + */ + void process_edges(PIO pio, uint sm_edge); + + +// FOR POOLING EVENTS +/** + * \brief Set PWM frequency + */ + void set_pwm(PIO pio, uint sm, float duty_cycle, uint32_t freq); + +/** + * \brief Camera output pin + */ + inline void set_pio_pwm_pin(uint8_t pwm_pio_pin) + { + gpio_deinit(DEFAULT_PIO_PWM_PIN); + // Init new gpio pin. + pwm_pio_pin_ = pwm_pio_pin; + gpio_init(pwm_pio_pin_); + gpio_set_dir(pwm_pio_pin_, 1); + sm_ = DEFAULT_PIO_SM; + uint offset = pio_add_program(pio0, &pwm_program); //FIXME pio0 is hard coded + pwm_init(pio0, sm_, offset, pwm_pio_pin_, false); + } + +/** + * \brief PWM frequency -- Camera FPS + */ + inline void set_pwm_freq(uint32_t pwm_freq) + {pwm_freq_ = pwm_freq;} + +/** + * \brief PWM duty cycle-- shouldn't change from 0.5f + */ + inline void set_pwm_duty_cycle(float pwm_duty) + {pwm_duty_ = pwm_duty;} + +/** + * \brief Enable Camera Triggering + */ + inline void set_enable_state(uint8_t enable_state) + {pio_sm_set_enabled(pio_, sm_, bool(enable_state));} + +// Read functions +/** + * \brief Get PIO PWM PIN + */ + inline uint8_t get_pio_pwm_pin() const + {return pwm_pio_pin_;} + +/** + * \brief Get the state of the PIO PWM pin + */ + inline float get_pwm_pin_state() const + {return pwm_pin_state_;} + +/** + * \brief Get PWM Frequency - current FPS + */ + inline uint32_t get_pwm_freq() const + {return pwm_freq_;} + +/** + * \brief Get PWM Duty Cycle + */ + inline float get_pwm_duty() const + {return pwm_duty_;} + +/** + * \brief Get camera trigger enable state + */ + inline float get_enable_state() const + {return enable_state_;} + +private: + +/** + * \brief Detect rise and fall events of PWM signal + */ + void pwm_signal_status(); + + // Declare data members + uint8_t pwm_pio_pin_; + uint8_t pwm_pin_state_; + uint8_t enable_state_; + uint32_t pwm_freq_; + float pwm_duty_; + bool pin_is_initialized_; + PIO pio_; + uint sm_; + + // Declare Constants + static inline constexpr float DEFAULT_DUTY_CYCLE = 0.5f; + static inline constexpr uint32_t DEFAULT_FREQ = 60; + static inline constexpr uint8_t DEFAULT_PIO_PWM_PIN = CAM_TRIGGER_PIN; + static inline constexpr uint DEFAULT_PIO_SM = 0; +}; + + + + + diff --git a/firmware/lib/etl b/firmware/lib/etl new file mode 160000 index 0000000..bda1e86 --- /dev/null +++ b/firmware/lib/etl @@ -0,0 +1 @@ +Subproject commit bda1e8619de7df97da7fc69594f0a65552f506b8 diff --git a/firmware/lib/harp.core.rp2040 b/firmware/lib/harp.core.rp2040 index 51f5d99..6ba5a7f 160000 --- a/firmware/lib/harp.core.rp2040 +++ b/firmware/lib/harp.core.rp2040 @@ -1 +1 @@ -Subproject commit 51f5d9995c88d41bd2580591e24bd70c70c28ea3 +Subproject commit 6ba5a7fa2df6b6b55238f4e69fd3b9073910d7ec diff --git a/firmware/src/delphi_controller_app.cpp b/firmware/src/delphi_controller_app.cpp new file mode 100644 index 0000000..0fdce33 --- /dev/null +++ b/firmware/src/delphi_controller_app.cpp @@ -0,0 +1,786 @@ +#include + +app_regs_t app_regs; +uint8_t old_aux_gpio_inputs; +uint8_t led_state; + + +// Create function aliases for readability. +void (&read_aux_gpio_dir)(uint8_t reg_address) = HarpCore::read_reg_generic; +void (&read_aux_gpio_set)(uint8_t reg_address) = HarpCore::read_reg_generic; +void (&read_aux_gpio_clear)(uint8_t reg_address) = HarpCore::read_reg_generic; + +void (&read_aux_gpio_rise_event)(uint8_t reg_address) = HarpCore::read_reg_generic; +void (&read_aux_gpio_fall_event)(uint8_t reg_address) = HarpCore::read_reg_generic; +void (&write_aux_gpio_rise_event)(msg_t& msg) = HarpCore::write_reg_generic; +void (&write_aux_gpio_fall_event)(msg_t& msg) = HarpCore::write_reg_generic; + +void (&read_aux_gpio_rise_input)(uint8_t reg_address) = HarpCore::read_reg_generic; +void (&read_aux_gpio_fall_input)(uint8_t reg_address) = HarpCore::read_reg_generic; + + +void (&read_force_fsm)(uint8_t reg_address) = HarpCore::read_reg_generic; + +/// Create Hit-and-Hold Valve Drivers. +/// The underlying PWM peripheral, aka: a PWM Slice, controls two adjacent PWM +/// pins and must be configured with the same settings. This is OK since we are +/// enforcing the same underlying peripheral settings (i.e: frequency) across +/// all Slices. +ValveDriver valve_drivers[NUM_VALVES] +{{VALVE_PIN_BASE}, + {VALVE_PIN_BASE + 1}, + {VALVE_PIN_BASE + 2}, + {VALVE_PIN_BASE + 3}, + {VALVE_PIN_BASE + 4}, + {VALVE_PIN_BASE + 5}, + {VALVE_PIN_BASE + 6}, + {VALVE_PIN_BASE + 7}, + {VALVE_PIN_BASE + 8}, + {VALVE_PIN_BASE + 9}, + {VALVE_PIN_BASE + 10}, + {VALVE_PIN_BASE + 11}, + {VALVE_PIN_BASE + 12}, + {VALVE_PIN_BASE + 13}, + {VALVE_PIN_BASE + 14}, + {VALVE_PIN_BASE + 15}}; + +// Define "specs" per-register +RegSpecs app_reg_specs[APP_REG_COUNT] +{ + {(uint8_t*)&app_regs.ValvesState, sizeof(app_regs.ValvesState), U16}, + {(uint8_t*)&app_regs.ValvesSet, sizeof(app_regs.ValvesSet), U16}, + {(uint8_t*)&app_regs.ValvesClear, sizeof(app_regs.ValvesClear), U16}, + + {(uint8_t*)&app_regs.ValveConfigs[0], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[1], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[2], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[3], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[4], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[5], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[6], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[7], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[8], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[9], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[10], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[11], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[12], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[13], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[14], sizeof(ValveConfig), U8}, + {(uint8_t*)&app_regs.ValveConfigs[15], sizeof(ValveConfig), U8}, + + {(uint8_t*)&app_regs.AuxGPIODir, sizeof(app_regs.AuxGPIODir), U8}, + {(uint8_t*)&app_regs.AuxGPIOState, sizeof(app_regs.AuxGPIOState), U8}, + {(uint8_t*)&app_regs.AuxGPIOSet, sizeof(app_regs.AuxGPIOSet), U8}, + {(uint8_t*)&app_regs.AuxGPIOClear, sizeof(app_regs.AuxGPIOClear), U8}, + + {(uint8_t*)&app_regs.AuxGPIOInputRiseEvent, sizeof(app_regs.AuxGPIOInputRiseEvent), U8}, + {(uint8_t*)&app_regs.AuxGPIOInputFallEvent, sizeof(app_regs.AuxGPIOInputFallEvent), U8}, + {(uint8_t*)&app_regs.AuxGPIORisingInputs, sizeof(app_regs.AuxGPIORisingInputs), U8}, + {(uint8_t*)&app_regs.AuxGPIOFallingInputs, sizeof(app_regs.AuxGPIOFallingInputs), U8}, + + // More specs here for poke manager registers. + {(uint8_t*)&app_regs.PokePin, sizeof(app_regs.PokePin), U8}, + {(uint8_t*)&app_regs.PokePinInverted, sizeof(app_regs.PokePinInverted), U8}, + {(uint8_t*)&app_regs.PokeState, sizeof(app_regs.PokeState), U8}, + {(uint8_t*)&app_regs.RawPokeState, sizeof(app_regs.RawPokeState), U8}, + {(uint8_t*)&app_regs.PokeDometer, sizeof(app_regs.PokeDometer), U32}, + {(uint8_t*)&app_regs.FSMEnabledState, sizeof(app_regs.FSMEnabledState), U8}, + {(uint8_t*)&app_regs.ForceFSM, sizeof(app_regs.ForceFSM), U8}, + {(uint8_t*)&app_regs.QueuedOdorMask, sizeof(app_regs.QueuedOdorMask), U16}, + {(uint8_t*)&app_regs.VacuumCloseTimeUS, sizeof(app_regs.VacuumCloseTimeUS), U32}, + {(uint8_t*)&app_regs.MinOdorDeliveryTimeUS, sizeof(app_regs.MinOdorDeliveryTimeUS), U32}, + {(uint8_t*)&app_regs.MaxOdorDeliveryTimeUS, sizeof(app_regs.MaxOdorDeliveryTimeUS), U32}, + {(uint8_t*)&app_regs.OdorTransitionTimeUS, sizeof(app_regs.OdorTransitionTimeUS), U32}, + {(uint8_t*)&app_regs.VacuumSetupTimeUS, sizeof(app_regs.VacuumSetupTimeUS), U32}, + {(uint8_t*)&app_regs.FinalValveEnergizedTimeUS, sizeof(app_regs.FinalValveEnergizedTimeUS), U32}, + {(uint8_t*)&app_regs.MinimumPokeTimeUS, sizeof(app_regs.MinimumPokeTimeUS), U32}, + {(uint8_t*)&app_regs.CamPin, sizeof(app_regs.CamPin), U8}, + {(uint8_t*)&app_regs.CamPinState, sizeof(app_regs.CamPinState), U8}, + {(uint8_t*)&app_regs.FrameRate, sizeof(app_regs.FrameRate), U32}, + {(uint8_t*)&app_regs.DutyCycle, sizeof(app_regs.DutyCycle), Float}, + {(uint8_t*)&app_regs.EnableCamTrigger, sizeof(app_regs.EnableCamTrigger), U8}, + {(uint8_t*)&app_regs.EnableValveLeds, sizeof(app_regs.EnableValveLeds), U8} +}; + +RegFnPair reg_handler_fns[APP_REG_COUNT] +{ + {read_valves_state, write_valves_state}, + {read_valves_set, write_valves_set}, + {read_valves_clear, write_valves_clear}, + + {read_any_valve_config, write_any_valve_config}, // valve 0 + {read_any_valve_config, write_any_valve_config}, // valve 1 + {read_any_valve_config, write_any_valve_config}, // ... + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, + {read_any_valve_config, write_any_valve_config}, // valve 15 + + {read_aux_gpio_dir, write_aux_gpio_dir}, + {read_aux_gpio_state, write_aux_gpio_state}, + {read_aux_gpio_set, write_aux_gpio_set}, + {read_aux_gpio_clear, write_aux_gpio_clear}, + + {read_aux_gpio_rise_event, write_aux_gpio_rise_event}, + {read_aux_gpio_fall_event, write_aux_gpio_fall_event}, + {read_aux_gpio_rise_input, HarpCore::write_to_read_only_reg_error}, + {read_aux_gpio_fall_input, HarpCore::write_to_read_only_reg_error}, + + // Poke manager handler functions + {read_poke_pin, write_poke_pin}, + {read_poke_pin_inverted, write_poke_pin_inverted}, + {read_poke_state, HarpCore::write_to_read_only_reg_error}, + {read_raw_poke_state, HarpCore::write_to_read_only_reg_error}, + {read_pokedometer, HarpCore::write_to_read_only_reg_error}, + {read_fsm_enabled_state, write_fsm_enabled_state}, + {read_force_fsm, write_force_fsm}, + {read_current_odors, write_current_odors}, + {read_vacuum_close_time_us, write_vacuum_close_time_us}, + {read_min_odor_delivery_time_us, write_min_odor_delivery_time_us}, + {read_max_odor_delivery_time_us, write_max_odor_delivery_time_us}, + {read_odor_transition_time_us, write_odor_transition_time_us}, + {read_vacuum_setup_time_us, write_vacuum_setup_time_us}, + {read_final_valve_energized_time_us, write_final_valve_energized_time_us}, + {read_minimum_poke_time_us, write_minimum_poke_time_us}, + {read_cam_pin, write_cam_pin}, //Start here + {read_cam_pin_state, HarpCore::write_to_read_only_reg_error}, + {read_frame_rate, write_frame_rate}, + {read_duty_cycle, write_duty_cycle}, + {read_enable_cam_trigger, write_enable_cam_trigger}, + {read_valve_leds, write_valve_leds} +}; + +void read_valve_leds(uint8_t reg_address) +{ + app_regs.EnableValveLeds = gpio_get(LED_ENABLE_PIN); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_valve_leds(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + gpio_put(LED_ENABLE_PIN, app_regs.EnableValveLeds); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_enable_cam_trigger(uint8_t reg_address) +{ + app_regs.EnableCamTrigger = cam_driver.get_enable_state(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_enable_cam_trigger(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + cam_driver.set_enable_state(app_regs.EnableCamTrigger); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_duty_cycle(uint8_t reg_address) +{ + app_regs.DutyCycle = cam_driver.get_pwm_duty(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_duty_cycle(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + cam_driver.set_pwm_duty_cycle(app_regs.DutyCycle); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_frame_rate(uint8_t reg_address) +{ + app_regs.FrameRate = cam_driver.get_pwm_freq(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_frame_rate(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + cam_driver.set_pwm_freq(app_regs.FrameRate); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_cam_pin(uint8_t reg_address) +{ + app_regs.CamPin = cam_driver.get_pio_pwm_pin(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_cam_pin(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + cam_driver.set_pio_pwm_pin(app_regs.CamPin); //disable previous camera pin + gpio_set_irq_enabled_with_callback(app_regs.CamPin, GPIO_IRQ_EDGE_RISE, true, &camera_timestamp_callback); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_cam_pin_state(uint8_t reg_address) +{ + app_regs.CamPinState = cam_driver.get_pwm_pin_state(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void read_poke_pin(uint8_t reg_address) +{ + app_regs.PokePin = poke_manager.get_poke_pin(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_poke_pin(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_poke_pin(app_regs.PokePin); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_poke_pin_inverted(uint8_t reg_address) +{ + app_regs.PokePinInverted = poke_manager.poke_pin_is_inverted(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_poke_pin_inverted(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_poke_pin_override_state(gpio_override(app_regs.PokePinInverted)); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_poke_state(uint8_t reg_address) +{ + // FIXME + app_regs.PokeState = poke_manager.get_poke_state(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void read_raw_poke_state(uint8_t reg_address) +{ + // FIXME + app_regs.RawPokeState = poke_manager.get_raw_poke_state(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void read_pokedometer(uint8_t reg_address) +{ + app_regs.PokeDometer = poke_manager.get_poke_count(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void read_fsm_enabled_state(uint8_t reg_address) +{ + // FIXME: doesn't exist. +// app_regs.FSMEnabledState = poke_manager.get_enabled_state(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_fsm_enabled_state(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_enabled_state(app_regs.FSMEnabledState); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void write_force_fsm(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.force_poke(); // FIXME: is this the correct way to force the fsm? + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_current_odors(uint8_t reg_address) +{ + // Get recent poke count value + app_regs.QueuedOdorMask = poke_manager.get_current_odors(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_current_odors(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_current_odors(app_regs.QueuedOdorMask); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_vacuum_close_time_us(uint8_t reg_address) +{ + app_regs.VacuumCloseTimeUS = poke_manager.get_vacuum_close_time_us(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_vacuum_close_time_us(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_vacuum_close_time_us(app_regs.VacuumCloseTimeUS); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_min_odor_delivery_time_us(uint8_t reg_address) +{ + app_regs.MinOdorDeliveryTimeUS = poke_manager.get_min_odor_delivery_time_us(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_min_odor_delivery_time_us(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_min_odor_delivery_time_us(app_regs.MinOdorDeliveryTimeUS); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_max_odor_delivery_time_us(uint8_t reg_address) +{ + app_regs.MaxOdorDeliveryTimeUS = poke_manager.get_max_odor_delivery_time_us(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_max_odor_delivery_time_us(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_max_odor_delivery_time_us(app_regs.MaxOdorDeliveryTimeUS); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_odor_transition_time_us(uint8_t reg_address) +{ + app_regs.OdorTransitionTimeUS = poke_manager.get_odor_transition_time_us(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_odor_transition_time_us(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_odor_transition_time_us(app_regs.OdorTransitionTimeUS); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_vacuum_setup_time_us(uint8_t reg_address) +{ + app_regs.VacuumSetupTimeUS = poke_manager.get_vacuum_setup_time_us(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_vacuum_setup_time_us(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_vacuum_setup_time_us(app_regs.VacuumSetupTimeUS); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_final_valve_energized_time_us(uint8_t reg_address) +{ + app_regs.FinalValveEnergizedTimeUS = + poke_manager.get_final_valve_energized_time_us(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_final_valve_energized_time_us(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_final_valve_energized_time_us(app_regs.FinalValveEnergizedTimeUS); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_minimum_poke_time_us(uint8_t reg_address) +{ + app_regs.MinimumPokeTimeUS = poke_manager.get_min_poke_time_us(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_minimum_poke_time_us(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + poke_manager.set_min_poke_time_us(app_regs.MinimumPokeTimeUS); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + + +void read_valves_state(uint8_t reg_address) +{ + app_regs.ValvesState = get_valve_mask(); // Store the mask + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_valves_state(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + for (size_t valve_index = 0; valve_index < NUM_VALVES; ++valve_index) + { + if ((app_regs.ValvesState >> valve_index) & (typeof(app_regs.ValvesState))(1)) + valve_drivers[valve_index].energize(); + else + valve_drivers[valve_index].deenergize(); + } + // Reply with the actual value that we wrote. + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_valves_set(uint8_t reg_address) +{ + // Return the most recently set value. + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_valves_set(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + for (size_t valve_index = 0; valve_index < NUM_VALVES; ++valve_index) + { + if ((app_regs.ValvesSet >> valve_index) & (typeof(app_regs.ValvesSet))(1)) + valve_drivers[valve_index].energize(); + } + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_valves_clear(uint8_t reg_address) +{ + // Return the most recently cleared value. + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_valves_clear(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + for (size_t valve_index = 0; valve_index < NUM_VALVES; ++valve_index) + { + if ((app_regs.ValvesClear >> valve_index) & (typeof(app_regs.ValvesClear))(1)) + valve_drivers[valve_index].deenergize(); + } + // Reply with the actual value that we wrote. + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + + +void read_any_valve_config(uint8_t reg_address) +{ + uint8_t valve_index = reg_address - VALVE_START_APP_ADDRESS; + ValveConfig& valve_cfg = app_regs.ValveConfigs[valve_index]; + const ValveDriver& valve_driver = valve_drivers[valve_index]; + // Update Harp App registers with ValveDriver class contents. + valve_cfg.hit_output = valve_driver.get_hit_output(); + valve_cfg.hold_output = valve_driver.get_hold_output(); + valve_cfg.hit_duration_us = valve_driver.get_hit_duration_us(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + + +void write_any_valve_config(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + uint8_t valve_index = msg.header.address - VALVE_START_APP_ADDRESS; + const ValveConfig& valve_cfg = app_regs.ValveConfigs[valve_index]; + ValveDriver& valve_driver = valve_drivers[valve_index]; + // Apply the configuration. + valve_driver.set_hit_duration_us(valve_cfg.hit_duration_us); + valve_driver.set_normalized_hit_output(valve_cfg.hit_output); + valve_driver.set_normalized_hold_output(valve_cfg.hold_output); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +//void read_aux_gpio_dir(uint8_t reg_address) +//{ +// // Nothing to do! +// // This register will stay consistent with the underlying peripheral +// // register after we initialize it the first time. +// if (!HarpCore::is_muted()) +// HarpCore::send_harp_reply(READ, reg_address); +//} + +void write_aux_gpio_dir(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + // Apply register settings (set bits are outputs; cleared bits are inputs). + gpio_set_dir_masked(uint32_t(GPIOS_MASK) << GPIO_PIN_BASE, + uint32_t(app_regs.AuxGPIODir) << GPIO_PIN_BASE); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void read_aux_gpio_state(uint8_t reg_address) +{ + // Update register contents. + app_regs.AuxGPIOState = read_aux_gpios(); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(READ, reg_address); +} + +void write_aux_gpio_state(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + // Note: only write to outputs + gpio_put_masked(uint32_t(app_regs.AuxGPIODir) << GPIO_PIN_BASE, + uint32_t(app_regs.AuxGPIOState) << GPIO_PIN_BASE); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void write_aux_gpio_set(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + // Note: only write to outputs (ie: mask on Set bits). + gpio_put_masked( + uint32_t(app_regs.AuxGPIODir & app_regs.AuxGPIOSet) << GPIO_PIN_BASE, + uint32_t(app_regs.AuxGPIOSet) << GPIO_PIN_BASE); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void write_aux_gpio_clear(msg_t& msg) +{ + HarpCore::copy_msg_payload_to_register(msg); + // Note: only write to outputs (ie: mask on Clear bits). + gpio_put_masked( + uint32_t(app_regs.AuxGPIODir & app_regs.AuxGPIOClear) << GPIO_PIN_BASE, + 0); + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(WRITE, msg.header.address); +} + +void request_next_odor() +{ + const uint8_t NEXT_ODOR_INDEX_ADDRESS = 66; // FIXME: this is hardcoded. + app_regs.QueuedOdorMask = 0; // Mark it as "used." + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(EVENT, NEXT_ODOR_INDEX_ADDRESS, HarpCore::harp_time_us_64()); +} + +void poke_state_changed() +{ + const uint8_t POKE_STATE_INDEX_ADDRESS = 61; // FIXME: this is hardcoded. + app_regs.PokeState = 1; // Mark it as "used." + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(EVENT, POKE_STATE_INDEX_ADDRESS, HarpCore::harp_time_us_64()); +} + +void raw_poke_rise() +{ + const uint8_t POKE_STATE_INDEX_ADDRESS = 62; // FIXME: this is hardcoded. + app_regs.RawPokeState = 1; // Mark it as "used." + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(EVENT, POKE_STATE_INDEX_ADDRESS, HarpCore::harp_time_us_64()); +} + +void raw_poke_fall() +{ + const uint8_t POKE_STATE_INDEX_ADDRESS = 62; // FIXME: this is hardcoded. + app_regs.RawPokeState = 0; // Mark it as "used." + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(EVENT, POKE_STATE_INDEX_ADDRESS, HarpCore::harp_time_us_64()); +} + +void camera_timestamp_callback(uint gpio, uint32_t events) { + // // Toggle LED for testing + // gpio_put(25, !gpio_get(25)); + push_event_from_isr(CAM_PIN_STATE_INDEX_ADDRESS, HarpCore::harp_time_us_64()); +} + +// Delphi specific functions +#define QUEUE_SIZE 128 +#define QUEUE_MASK (QUEUE_SIZE - 1) +HarpEvent eventQueue[QUEUE_SIZE]; +volatile uint8_t head = 0; +volatile uint8_t tail = 0; + +void push_event_from_isr(uint8_t index, uint64_t timestamp) { + uint8_t next = (head + 1) & QUEUE_MASK; + if (next != tail) { // Prevent overflow + eventQueue[head].index = index; + eventQueue[head].timestamp = timestamp; + head = next; + } +} + +bool pop_event(HarpEvent &event) { + if (tail == head) return false; // Queue is empty + event.index = eventQueue[tail].index; + event.timestamp = eventQueue[tail].timestamp; + tail = (tail + 1) & QUEUE_MASK; + return true; +} + +// Valve state mask +uint16_t get_valve_mask() { + uint16_t mask = 0; // Start with all bits cleared + + for (size_t valve_index = 0; valve_index < NUM_VALVES && valve_index < 16; ++valve_index) // limit considers num valves and bit mask size + { + if (valve_drivers[valve_index].is_energized()) + { + // mask |= (uint16_t)(1) << valve_index; // Set bit for this valve + mask |= (1u << valve_index); + } + } + return mask; +} + +uint16_t previous_mask = 0; // Initialize to zero or read initial state + +void update_app_state() // Called when app.run() is called -- add poke detection here +{ + // Update valve controller state machines. + for (auto& valve_driver: valve_drivers) + valve_driver.update(); + + // Update poke manager FSM + poke_manager.update(); + + // Update Camera Driver FSM + cam_driver.update(); + + // Handle harp events + HarpEvent evt; + while (pop_event(evt)) { + if (!HarpCore::is_muted()) { + HarpCore::send_harp_reply(EVENT, evt.index, evt.timestamp); + } + } + + // Handle valve state changes + uint16_t current_mask = get_valve_mask(); + if (current_mask != previous_mask) { + app_regs.ValvesState = current_mask; + if (!HarpCore::is_muted()) + HarpCore::send_harp_reply(EVENT, VALVES_STATE_INDEX_ADDRESS, HarpCore::harp_time_us_64()); + previous_mask = current_mask; + } + + // Process AuxGPIO input changes. + // FIXME: do we need to update old_aux_gpio_inputs if we change (write-to) + uint8_t aux_gpio_inputs = read_aux_gpios() & ~app_regs.AuxGPIODir; + uint8_t changed_inputs = (old_aux_gpio_inputs ^ aux_gpio_inputs); + app_regs.AuxGPIORisingInputs = app_regs.AuxGPIOInputRiseEvent & aux_gpio_inputs & changed_inputs; + app_regs.AuxGPIOFallingInputs = app_regs.AuxGPIOInputFallEvent & ~aux_gpio_inputs & changed_inputs; + old_aux_gpio_inputs = aux_gpio_inputs; + // Emit EVENT messages for rising/falling edges on configured pins. + if (HarpCore::is_muted()) + return; + if (app_regs.AuxGPIOInputRiseEvent & app_regs.AuxGPIORisingInputs) + HarpCore::send_harp_reply(EVENT, AUX_GPIO_RISING_INPUTS_ADDRESS); + if (app_regs.AuxGPIOInputFallEvent & app_regs.AuxGPIOFallingInputs) + HarpCore::send_harp_reply(EVENT, AUX_GPIO_FALLING_INPUTS_ADDRESS); +} + +void reset_app() +{ + // Reset poke manager and all poke-manager-related registers + poke_manager.reset(); + poke_manager.set_next_odor_callback_fn(request_next_odor); + poke_manager.set_poke_state_callback_fn(poke_state_changed); + poke_manager.set_raw_poke_rise_callback_fn(raw_poke_rise); + poke_manager.set_raw_poke_fall_callback_fn(raw_poke_fall); + app_regs.PokeDometer = poke_manager.get_poke_count(); + app_regs.FSMEnabledState = poke_manager.get_enabled_state(); + app_regs.ForceFSM = 0; + app_regs.QueuedOdorMask = poke_manager.get_current_odors(); + app_regs.VacuumCloseTimeUS = poke_manager.get_vacuum_close_time_us(); + app_regs.MinOdorDeliveryTimeUS = poke_manager.get_min_odor_delivery_time_us(); + app_regs.MaxOdorDeliveryTimeUS = poke_manager.get_max_odor_delivery_time_us(); + app_regs.OdorTransitionTimeUS = poke_manager.get_odor_transition_time_us(); + app_regs.VacuumSetupTimeUS = poke_manager.get_vacuum_setup_time_us(); + app_regs.FinalValveEnergizedTimeUS = poke_manager.get_final_valve_energized_time_us(); + app_regs.MinimumPokeTimeUS = poke_manager.get_min_poke_time_us(); + + //Reset cam driver and all related registers + cam_driver.reset(); + // cam_driver.set_pwm_rise_callback_fn(rising_edge_detected); //USED FOR POOLING EVENTS + // cam_driver.set_pwm_fall_callback_fn(falling_edge_detected); // USED FOR POOLING EVENTS + app_regs.CamPinState = cam_driver.get_pwm_pin_state(); + app_regs.FrameRate = cam_driver.get_pwm_freq(); + app_regs.DutyCycle = cam_driver.get_pwm_duty(); + app_regs.EnableCamTrigger = cam_driver.get_enable_state(); + + // FOR TESTING -- LED blinking + // gpio_init(LED_ENABLE_PIN); + // gpio_set_dir(LED_ENABLE_PIN, GPIO_OUT); + + // Valve LED state + gpio_init(LED_ENABLE_PIN); + gpio_set_dir(LED_ENABLE_PIN, GPIO_OUT); + gpio_put(LED_ENABLE_PIN, 0); + + // Reset Harp register struct elements. + app_regs.ValvesState = 0; + app_regs.ValvesSet = 0; + app_regs.ValvesClear = 0; + // Turn off all outputs. + for (auto& valve_driver: valve_drivers) + valve_driver.reset(); + + // Init the exposed auxiliary GPIO pins we are using as 4-inputs. + // This *must* be called once to setup the AUX GPIOs. + gpio_init_mask(GPIOS_MASK << GPIO_PIN_BASE); + gpio_set_dir_masked(GPIOS_MASK << GPIO_PIN_BASE, 0); + + app_regs.AuxGPIODir = 0b11110000; // GPIO pins 25-29 as outputs + app_regs.AuxGPIOState = (gpio_get_all() >> GPIO_PIN_BASE) & GPIOS_MASK; //all pins are set low + app_regs.AuxGPIOSet = 0; + app_regs.AuxGPIOClear = 0; + + gpio_set_dir_masked(uint32_t(GPIOS_MASK) << GPIO_PIN_BASE, + uint32_t(app_regs.AuxGPIODir) << GPIO_PIN_BASE); + + // Clear aux input EVENT message configuration. + app_regs.AuxGPIORisingInputs = 0; + app_regs.AuxGPIOFallingInputs = 0; + + old_aux_gpio_inputs = read_aux_gpios() & ~app_regs.AuxGPIODir; + + // gpio_set_irq_enabled_with_callback(26, GPIO_IRQ_EDGE_RISE, true, &camera_timestamp_callback); + +} + diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 3508d32..303b318 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -4,12 +4,15 @@ #include #include #include -#include +#include +#include +#include +#include +#include #ifdef DEBUG #include // for uart printing #include // for printf #endif - // Create Harp App. HarpCApp& app = HarpCApp::init(HARP_DEVICE_ID, HW_VERSION_MAJOR, HW_VERSION_MINOR, @@ -17,16 +20,28 @@ HarpCApp& app = HarpCApp::init(HARP_DEVICE_ID, HARP_VERSION_MAJOR, HARP_VERSION_MINOR, FW_VERSION_MAJOR, FW_VERSION_MINOR, UNUSED_SERIAL_NUMBER, - "valve-controller", + "delphi-controller", (uint8_t*)GIT_HASH, &app_regs, app_reg_specs, reg_handler_fns, APP_REG_COUNT, update_app_state, reset_app); + ValveDriver& final_valve = valve_drivers[FINAL_VALVE_INDEX]; + ValveDriver& vac_valve = valve_drivers[VACCUM_VALVE_INDEX]; + // Consider the rest of the valves as odor delivery valves. + ValveDriver* odor_valves_start = valve_drivers + 2; + ValveDriver (&odor_valves)[] = *reinterpret_cast(odor_valves_start); + +// Pass valves into the poke manager constructor +PokeManager poke_manager(final_valve, vac_valve, odor_valves, NUM_ODOR_VALVES); + +// Select Cam pin for the CAM DRIVER constuctor +CameraDriver cam_driver(CAM_TRIGGER_PIN); + // Core0 main. int main() { -// Init Synchronizer. + // Init Synchronizer. HarpSynchronizer::init(uart1, HARP_SYNC_RX_PIN); app.set_synchronizer(&HarpSynchronizer::instance()); #ifdef DEBUG diff --git a/firmware/src/poke_manager.cpp b/firmware/src/poke_manager.cpp new file mode 100644 index 0000000..94e6811 --- /dev/null +++ b/firmware/src/poke_manager.cpp @@ -0,0 +1,259 @@ +#include + +PokeManager::PokeManager(ValveDriver& final_valve, ValveDriver& vac_valve, + ValveDriver (&odor_valves)[], size_t num_odor_valves) +: final_valve_{final_valve}, vac_valve_{vac_valve}, odor_valves_{odor_valves}, +num_odor_valves_{num_odor_valves}, +state_{RESET}, poke_count_{0}, poke_pin_{DEFAUT_POKE_PIN}, +odor_valve_mask_{0}, disable_fsm_{false}, +poke_detected_{false}, poke_state_{0}, raw_poke_state_{0}, +beam_broken_{false}, poke_initiated_once_{false}, +request_next_odor_callback_fn_{nullptr}, request_poke_state_callback_fn_{nullptr}, +request_raw_poke_rise_callback_fn_{nullptr}, request_raw_poke_fall_callback_fn_{nullptr}, +poke_pin_is_initialized_{false}, valve_state_{false} +{ + reset(); // set timing constants to defaults. +} + +PokeManager::~PokeManager() //destuctor +{ + //Deengergize all valves + deenergize_all_valves(); + poke_count_ = 0; + poke_state_ = 0; + raw_poke_state_ = 0; + poke_detected_ = false; + disable_fsm_ = false; + state_ = RESET; + beam_broken_ = false; + poke_initiated_once_ = false; + valve_state_ = false; +} + +void PokeManager::deenergize_all_valves() +{ + final_valve_.deenergize(); + vac_valve_.deenergize(); + for (int i = 0; i < num_odor_valves_; ++i) + odor_valves_[i].deenergize(); +} + +//Poke detection function +void PokeManager::update_poke_status() +{ + // Check to see if poke has been detected + // Beam is no longer broken + if (!gpio_get(poke_pin_)) + { + beam_broken_ == false; + poke_start_time_us_ = time_us_32(); + poke_initiated_once_ = false; + + if (raw_poke_state_ == 1) + { + //falling edge event + raw_poke_fall(); + } + raw_poke_state_ = 0; + } + + // Beam broken -- update raw poke state + if (gpio_get(poke_pin_)) + { + if (raw_poke_state_ == 0) + { + //rising edge event + raw_poke_rise(); + } + raw_poke_state_ = 1; + } + + // Poke detected during -- start poke timer + if (gpio_get(poke_pin_) && !beam_broken_ && state_ == ODOR_DISPENSING_TO_EXHAUST) + { + poke_start_time_us_ = time_us_32(); + beam_broken_ = true; + } + + // Check duration since beam break/poke + if (gpio_get(poke_pin_) && beam_broken_ && state_ == ODOR_DISPENSING_TO_EXHAUST) + { + //gpio_put(LED_PIN, 1); // Turn on LED whenever the beam is broken + if ((time_us_32() - poke_start_time_us_) >= min_poke_time_us_ && poke_initiated_once_ == false) + { + //Poke was detected! + poke(); + poke_state_changed(); + poke_state_ = 1; +#if(DEBUG) + printf("Poke detected!\r\n"); +#endif + //Account for the successful poke so that another doesn't occur on the same poke + poke_initiated_once_ = true; + } + } +} + +// Functions to alter the FSM +void PokeManager::reset() +{ + state_ = RESET; + deenergize_all_valves(); + disable(); + odor_valve_mask_ = 0; + poke_count_ = 0; + poke_state_ = 0; + poke_detected_ = false; + beam_broken_ = false; + poke_initiated_once_ = false; + valve_state_ = false; + clear_poke_pin(); + request_next_odor_callback_fn_ = nullptr; + request_poke_state_callback_fn_ = nullptr; + request_raw_poke_rise_callback_fn_ = nullptr; + request_raw_poke_fall_callback_fn_ = nullptr; + set_vacuum_close_time_us(DEFAULT_VACUUM_CLOSE_TIME_US); + set_min_odor_delivery_time_us(DEFAULT_MIN_ODOR_DELIVERY_TIME_US); + set_max_odor_delivery_time_us(DEFAULT_MAX_ODOR_DELIVERY_TIME_US); + set_odor_transition_time_us(DEFAULT_ODOR_TRANSITION_TIME_US); + set_vacuum_setup_time_us(DEFAULT_VACUUM_SETUP_TIME_US); + set_final_valve_energized_time_us(DEFAULT_FINAL_VALVE_ENERGIZED_TIME_US); +} + +void PokeManager::set_enabled_state(bool enabled) +{ + if (enabled) + disable_fsm_ = false; + else + { + disable_fsm_ = true; + deenergize_all_valves(); // deenergize all valves + // Clear internal state machine variables. + state_ = RESET; + poke_detected_ = false; + poke_state_ = 0; + beam_broken_ = false; + poke_initiated_once_ = false; + valve_state_ = false; + } +} + +void PokeManager::update() +{ + //enabled by default, but if disabled, bail early + if (disable_fsm_) + return; + + // check for poke + update_poke_status(); + + state_t next_state{state_}; // initialize next-state to current state. + + // Handling next-state logic. + switch (state_) + { + case RESET: + //initialize RESET state by turning off all valves + deenergize_all_valves(); + next_state = ODOR_SETUP; + break; + case ODOR_SETUP: + if (odor_valve_mask_ == 0){ + // The odor should be primed before a poke + poke_detected_ = false; + poke_state_ = 0; + } + + else if (odor_valve_mask_ > 0 && !valve_state_){ + energize_odor_valve(); // Need to initiated the event that the odor was consumed before this + } + else if (state_duration_us() >= vacuum_close_time_us_ && odor_valve_mask_ != 0) + { + next_state = ODOR_DISPENSING_TO_EXHAUST; + } + break; + case ODOR_DISPENSING_TO_EXHAUST: + if (poke_detected_) // poke detected and an odor is primed + { + next_state = ODOR_DELIVERY_TO_FINAL_VALVE; + } + break; + case ODOR_DELIVERY_TO_FINAL_VALVE: + if ((state_duration_us() >= min_odor_delivery_time_us_ && !poke_initiated_once_) || state_duration_us() >= max_odor_delivery_time_us_) //adjust to determine if the beam is still broken after the poke (up to max) + next_state = ODOR_PRECLEAN; + break; + case ODOR_PRECLEAN: + if (state_duration_us() >= odor_transition_time_us_) + next_state = VAC_START; + break; + case VAC_START: + if (state_duration_us() >= vac_setup_time_us_) + next_state = ODOR_PURGE; + break; + case ODOR_PURGE: + if (state_duration_us() >= final_valve_energized_time_us_) + next_state = ODOR_SETUP; + break; + default: + break; + } + + // Update how long we've been in the new state. + if (state_ != next_state) + { +#if(DEBUG) + printf("State transition %d -> %d\r\n", state_, next_state); + printf("State transition time %i\r\n", state_duration_us()); +#endif + state_entry_time_us_ = time_us_32(); + + // Next state logic should only be assessed if there is a state transition + if (next_state == ODOR_SETUP) + { + // explicitly grab odor in the queue + deenergize_all_valves(); + } + + // Don't need to do anything because odor is being sent to exhaust and + // we are waiting for a poke + if (next_state == ODOR_DISPENSING_TO_EXHAUST){} + + if (next_state == ODOR_DELIVERY_TO_FINAL_VALVE) + { + final_valve_.energize(); + ++poke_count_; +#if(DEBUG) + printf("Number of pokes = %i\r\n", poke_count_); +#endif + poke_detected_ = false; + poke_state_ = 0; + } + + if (next_state == ODOR_PRECLEAN) + { + final_valve_.deenergize(); + } + + if (next_state == VAC_START) + { + deenergize_odor_valve(); + vac_valve_.energize(); + } + + if (next_state == ODOR_PURGE) + { + // Energize the final valve + final_valve_.energize(); + odor_valve_mask_ = 0; // Clear the mask + request_next_odor(); //request + +#if(DEBUG) + printf("Odor Valves: %i\r\n", odor_valve_mask_); //valve odor index +#endif + } + } + // Update state: + state_ = next_state; + + +} diff --git a/firmware/src/poke_manager_test.cpp b/firmware/src/poke_manager_test.cpp new file mode 100644 index 0000000..0b00ba9 --- /dev/null +++ b/firmware/src/poke_manager_test.cpp @@ -0,0 +1,122 @@ +#include + +PokeManager::PokeManager(ValveDriver& final_valve, ValveDriver& vac_valve, etl::vector& odor_valves) +: state_{RESET}, poke_count_{0}, valve_index_{0}, poke_detected_{false}, final_valve_{final_valve}, vac_valve_{vac_valve}, odor_valves_{odor_valves} +{ + // Nothing else to do! +} + +PokeManager::~PokeManager() //destuctor +{ + //Deengergize all valves + deenergize_all_valves(); +} + +void PokeManager::deenergize_all_valves() +{ + final_valve_.deenergize(); + vac_valve_.deenergize(); + for (int i = 0; i < NUM_ODOR_VALVES; i++){ + odor_valves_[i].deenergize(); + } +} + +void PokeManager::update() +{ + //initialize RESET state + if (state_ == RESET) //need to query state_ for initital + { + // state_entry_time_us_ = time_us_32(); + deenergize_all_valves(); + } + + state_t next_state{state_}; // initialize next-state to current state. + + // Handling next-state logic. + switch (state_) + { + case RESET: + next_state = ODOR_SETUP; + break; + case ODOR_SETUP: + if (state_duration_us() >= VACUUM_CLOSE_TIME_US) + next_state = ODOR_DISPENSING_TO_EXHAUST; + break; + case ODOR_DISPENSING_TO_EXHAUST: + if (poke_detected_) + next_state = ODOR_DELIVERY_TO_FINAL_VALVE; + break; + case ODOR_DELIVERY_TO_FINAL_VALVE: + if (state_duration_us() >= ODOR_DELIVERY_TIME_US) + next_state = ODOR_PRECLEAN; + break; + case ODOR_PRECLEAN: + if (state_duration_us() >= ODOR_TRANSITION_TIME_US) + next_state = VAC_START; + break; + case VAC_START: + if (state_duration_us() >= VAC_SETUP_TIME_US) + next_state = ODOR_PURGE; + break; + case ODOR_PURGE: + if (state_duration_us() >= FINAL_VALVE_ENERGIZED_TIME_US) + next_state = ODOR_SETUP; + break; + default: + break; + } + + // Update how long we've been in the new state. + if (state_ != next_state) + { + printf("State transition %d -> %d\r\n", state_, next_state); + printf("State transition time %i\r\n", state_duration_us()); + state_entry_time_us_ = time_us_32(); + + // Next state logic should only be assessed if there is a state transition + if (next_state == ODOR_SETUP) + { + // Energize one of the odor valves + deenergize_all_valves(); + odor_valves_[valve_index_].energize(); + } + + if (next_state == ODOR_DISPENSING_TO_EXHAUST) + { + // Don't need to do anything because odor is being sent to exhaust and we are waiting for a poke + } + + if (next_state == ODOR_DELIVERY_TO_FINAL_VALVE) + { + final_valve_.energize(); + ++poke_count_; + printf("Number of pokes = %i\r\n", poke_count_); + poke_detected_ = false; + } + + if (next_state == ODOR_PRECLEAN) + { + final_valve_.deenergize(); + } + + if (next_state == VAC_START) + { + // Deenergize odor valve + odor_valves_[valve_index_].deenergize(); + vac_valve_.energize(); + } + + if (next_state == ODOR_PURGE) + { + // Energize the final valve + final_valve_.energize(); + ++valve_index_; + if (valve_index_ == NUM_ODOR_VALVES) // test logic for iterating through valves + valve_index_ = 0; + + printf("Odor Valve: %i\r\n", valve_index_); //valve odor index + } + } + // Update state: + state_ = next_state; +} diff --git a/firmware/src/pwm_pio.cpp b/firmware/src/pwm_pio.cpp new file mode 100644 index 0000000..590168d --- /dev/null +++ b/firmware/src/pwm_pio.cpp @@ -0,0 +1,63 @@ +#include + +CameraDriver::CameraDriver(uint8_t pwm_pio_pin) +: pwm_pio_pin_{pwm_pio_pin}, pwm_freq_{DEFAULT_FREQ}, +pwm_duty_{DEFAULT_DUTY_CYCLE}, pin_is_initialized_{false}, +sm_{DEFAULT_PIO_SM}, pio_{pio0}, pwm_pin_state_{0}, +enable_state_{0} // request_pwm_rise_callback_fn_{nullptr}, request_pwm_fall_callback_fn_{nullptr} +{ + reset(); // set timing constants to defaults. +} + +CameraDriver::~CameraDriver() //destuctor +{ + pwm_freq_ = 0; + pwm_pin_state_ = 0; + pio_sm_set_enabled(pio_, sm_, false); +} + +// Functions to alter the FSM +void CameraDriver::reset() +{ + // request_pwm_rise_callback_fn_ = nullptr; // FOR POOLING EVENTS + // request_pwm_fall_callback_fn_ = nullptr; + sm_ = DEFAULT_PIO_SM; + uint offset = pio_add_program(pio_, &pwm_program); + pwm_init(pio_, sm_, offset, pwm_pio_pin_, enable_state_); + pwm_freq_ = DEFAULT_FREQ; + pwm_duty_ = DEFAULT_DUTY_CYCLE; //50% duty cycle +} + +void CameraDriver::set_pwm(PIO pio, uint sm, float duty_cycle, uint32_t freq) +{ + // const uint32_t total_us = 16667; // 60 Hz period in µs + uint32_t sys_clk = clock_get_hz(clk_sys); //125000000; + uint32_t period = sys_clk / freq; + uint32_t high_us = (uint32_t)(period * duty_cycle); + uint32_t low_us = period - high_us; + pio_sm_put_blocking(pio, sm, high_us); + pio_sm_put_blocking(pio, sm, low_us); +} + +void CameraDriver::pwm_init(PIO pio, uint sm, uint offset, uint8_t pin, uint8_t enable_state) + { + pio_gpio_init(pio, pin); + pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); + pio_sm_config c = pwm_program_get_default_config(offset); + sm_config_set_clkdiv(&c, 1.0f); // full speed + sm_config_set_set_pins(&c, pin, 1); + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_NONE); + pio_sm_init(pio, sm, offset, &c); + pio_sm_set_enabled(pio, sm, enable_state); +} + + +void CameraDriver::update() +{ + // set PWM + if (!pio_sm_is_tx_fifo_full(pio_, sm_)) + { + set_pwm(pio_, sm_, pwm_duty_, pwm_freq_); + } +} + diff --git a/firmware/src/valve_controller_app.cpp b/firmware/src/valve_controller_app.cpp index 937d2e9..7f8533b 100644 --- a/firmware/src/valve_controller_app.cpp +++ b/firmware/src/valve_controller_app.cpp @@ -309,5 +309,6 @@ void reset_app() app_regs.AuxGPIOFallingInputs = 0; old_aux_gpio_inputs = read_aux_gpios() & ~app_regs.AuxGPIODir; + } diff --git a/firmware/tests/poke_manager/CMakeLists.txt b/firmware/tests/poke_manager/CMakeLists.txt new file mode 100644 index 0000000..06b20bf --- /dev/null +++ b/firmware/tests/poke_manager/CMakeLists.txt @@ -0,0 +1,70 @@ +# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work == +if(WIN32) + set(USERHOME $ENV{USERPROFILE}) +else() + set(USERHOME $ENV{HOME}) +endif() +set(sdkVersion 2.1.1) +set(toolchainVersion 14_2_Rel1) +set(picotoolVersion 2.1.1) +set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake) +if (EXISTS ${picoVscode}) + include(${picoVscode}) +endif() +# ==================================================================================== +set(PICO_BOARD pico CACHE STRING "Board type") + +cmake_minimum_required(VERSION 3.13) +find_package(Git REQUIRED) +execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --short HEAD OUTPUT_VARIABLE COMMIT_ID OUTPUT_STRIP_TRAILING_WHITESPACE) +message(STATUS "Computed Git Hash: ${COMMIT_ID}") +add_definitions(-DGIT_HASH="${COMMIT_ID}") # Usable in source code. + +add_definitions(-DDEBUG) # Uncomment for debugging +add_definitions(-DUSBD_MANUFACTURER="Allen Institute") +add_definitions(-DUSBD_PRODUCT="test-poke_manager") + +# PICO_SDK_PATH must be defined. +include(${PICO_SDK_PATH}/pico_sdk_init.cmake) + +# Use modern conventions like std::invoke +set(CMAKE_CXX_STANDARD 17) + +project(poke-manager) + +pico_sdk_init() + +add_subdirectory(../../lib/rp2040.pwm build/rp2040_pwm) # build in this folder +add_subdirectory(../../lib/etl build/etl) + +add_executable(${PROJECT_NAME} + main.cpp +) + +add_library(poke_manager + ../../src/poke_manager.cpp +) + +add_library(valve_driver + ../../src/valve_driver.cpp +) +include_directories(../../inc) + +target_link_libraries(valve_driver rp2040_pwm pico_stdlib) + +target_link_libraries(poke_manager pico_stdlib valve_driver etl::etl) + +target_link_libraries(${PROJECT_NAME} + poke_manager pico_stdlib valve_driver etl::etl) + +pico_add_extra_outputs(${PROJECT_NAME}) + +message(WARNING "Debug printf() messages from harp core to UART with baud \ + rate 921600.") +pico_enable_stdio_usb(${PROJECT_NAME} 1) +pico_enable_stdio_usb(poke_manager 1) +# pico_enable_stdio_uart(${PROJECT_NAME} 1) # UART stdio for printf. +# pico_enable_stdio_uart(poke_manager 1) # UART stdio for printf. +# Additional libraries need to have stdio init also. + + diff --git a/firmware/tests/poke_manager/main.cpp b/firmware/tests/poke_manager/main.cpp new file mode 100644 index 0000000..cde8c1f --- /dev/null +++ b/firmware/tests/poke_manager/main.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include +#include +#include // for uart printing +#include // for printf + + +ValveDriver valve_drivers[NUM_VALVES] +{{VALVE_PIN_BASE}, + {VALVE_PIN_BASE + 1}, + {VALVE_PIN_BASE + 2}, + {VALVE_PIN_BASE + 3}, + {VALVE_PIN_BASE + 4}, + {VALVE_PIN_BASE + 5}, + {VALVE_PIN_BASE + 6}, + {VALVE_PIN_BASE + 7}, + {VALVE_PIN_BASE + 8}, + {VALVE_PIN_BASE + 9}, + {VALVE_PIN_BASE + 10}, + {VALVE_PIN_BASE + 11}, + {VALVE_PIN_BASE + 12}, + {VALVE_PIN_BASE + 13}, + {VALVE_PIN_BASE + 14}, + {VALVE_PIN_BASE + 15}}; + +ValveDriver& final_valve = valve_drivers[0]; // add to config +ValveDriver& vac_valve = valve_drivers[1]; +// Consider the rest of the valves as odor delivery valves. +ValveDriver* odor_valves_start = valve_drivers + 2; +ValveDriver (&odor_valves)[] = *reinterpret_cast(odor_valves_start); + +// Pass valves into the poke manager constructor +PokeManager poke_manager(final_valve, vac_valve, odor_valves, NUM_ODOR_VALVES); + + +// LED and Poke Port +const uint LED_PIN = 2;//25; +const uint POKE_PIN = 22; //GPIO pin for pokes + + +void request_next_odor() +{ + printf("Next odor, please!\r\n"); +} + +// Core0 main. +int main() +{ + gpio_init(LED_PIN); + gpio_set_dir(LED_PIN, GPIO_OUT); + + stdio_usb_init(); + stdio_set_translate_crlf(&stdio_usb, false); // Don't replace outgoing chars. + while (!stdio_usb_connected()){} // Block until connection to serial port. + printf("Hello, from an RP2040!\r\n"); + poke_manager.set_poke_pin(POKE_PIN); + poke_manager.set_poke_pin_override_state(GPIO_OVERRIDE_INVERT); + poke_manager.set_next_odor_callback_fn(request_next_odor); + poke_manager.set_enabled_state(true); + while(true){ + poke_manager.update(); // update through FSM + } +} diff --git a/software/pyharp/app_registers.py b/software/pyharp/app_registers.py index db935f7..444164b 100644 --- a/software/pyharp/app_registers.py +++ b/software/pyharp/app_registers.py @@ -1,6 +1,7 @@ """ValveController app registers. Later these will be extracted from the device.yaml""" -from enum import IntEnum +from enum import IntEnum +from itertools import chain class AppRegs(IntEnum): @@ -32,3 +33,32 @@ class AppRegs(IntEnum): AuxGPIOInputFallEvent = 56 AuxGPIOInputRisingInputs = 57 AuxGPIOFallingInputs = 58 + + +class DelphiOnlyAppRegs(IntEnum): + PokePin = 59 + PokePinInverted = 60 + PokeState = 61 + RawPokeState = 62 + PokeDometer = 63 + FSMEnabledState = 64 + ForceFSM = 65 + QueuedOdorMask = 66 + VacuumCloseTimeUS = 67 + MinOdorDeliveryTimeUS = 68 + MaxOdorDeliveryTimeUS = 69 + OdorTransitionTimeUS = 70 + VacuumSetupTimeUS = 71 + FinalValveEnergizedTimeUS = 72 + MinimumPokeTimeUS = 73 + CamPin = 74 + CamPinState = 75 + FrameRate = 76 + DutyCycle = 77 + EnableCamTrigger = 78 + EnableValveLeds = 79 + + +DelphiAppRegs = IntEnum( + "DelphiAppRegs", [(i.name, i.value) for i in chain(AppRegs, DelphiOnlyAppRegs)] +) diff --git a/software/pyharp/test_valve_timings.py b/software/pyharp/test_valve_timings.py new file mode 100644 index 0000000..7b6b11f --- /dev/null +++ b/software/pyharp/test_valve_timings.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +from pyharp.device import Device +from pyharp.messages import HarpMessage +from app_registers import AppRegs, DelphiOnlyAppRegs + +import logging + +logger = logging.getLogger() +logger.addHandler(logging.StreamHandler()) + + +# functions +def print_poke_counts( + device, +): + reply = device.send(HarpMessage.ReadU8(DelphiOnlyAppRegs.PokeDometer).frame) + print(f"Current pokedometer count is: {reply.payload}.") + return None + + +# Open serial connection with the first Valve Controller. +com_port = "COM5" #'COM3' #None +device = Device(com_port) +device.info() # Display device's info on screen + +print() +print("Enabling aux gpios 25-29 as outputs") +# gpio_dir = 32 +# reply = device.send(HarpMessage.WriteU8(AppRegs.AuxGPIODir, gpio_dir).frame) +# print(f"reply: {reply.payload[0]:08b}") +gpio_set = 0b00100000 +reply = device.send(HarpMessage.WriteU8(AppRegs.AuxGPIOSet, gpio_set).frame) +print(f"reply: {reply}") +print(f"reply: {reply.payload[0]:08b}") +# reply = device.send(HarpMessage.WriteU8(AppRegs.AuxGPIOClear, gpio_set).frame) +# print(f"reply: {reply.payload[0]:08b}") + + +print() +print_poke_counts(device) +print("Setting odor.") +reply = device.send( + HarpMessage.WriteU16(DelphiOnlyAppRegs.QueuedOdorMask, 0x0001).frame +) +print("Assigning poke pin.") +reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.PokePin, 22).frame) +print("Inverting poke pin.") +reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.PokePinInverted, 1).frame) +print("Enabling FSM") +reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.FSMEnabledState, 1).frame) +print("Camera Pin") +reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.CamPin, 26).frame) +print("FPS") +reply = device.send(HarpMessage.WriteU32(DelphiOnlyAppRegs.FrameRate, 100).frame) +print("Duty Cycle") +reply = device.send(HarpMessage.WriteFloat(DelphiOnlyAppRegs.DutyCycle, 0.5).frame) +print("Enable") +reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.EnableCamTrigger, 1).frame) +print("Enable Valve LEDS") +reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.EnableValveLeds, 0).frame) + +"""Set Timings""" +# print("Set Odor Delivery Time") +# reply = device.send(HarpMessage.WriteU32(DelphiOnlyAppRegs.OdorDeliveryTimeUS, 1000000).frame) +# print("Set Final Valve Energized Time") +# reply = device.send(HarpMessage.WriteU32(DelphiOnlyAppRegs.FinalValveEnergizedTimeUS, 20000).frame) +print("Min Poke Time") +reply = device.send( + HarpMessage.WriteU32(DelphiOnlyAppRegs.MinimumPokeTimeUS, 10000).frame +) + +print() +odor_masks = [0x0002, 0x0004, 0x0008, 0x0003, 0x000F] +print(odor_masks) +odor_i = -1 +try: + while True: + for msg in device.get_events(): + # print(msg) + # print() + # print_poke_counts(device) + # print(f"event address: {msg.address}") + # print(f"event payload: {msg.payload[0]}") + + """EVENT BASED ODOR UPDATING""" + event_address = msg.address + if event_address == 66: + event_payload = msg.payload[0] + if event_payload == 0: # -1 previously + odor_i += 1 + if odor_i > len(odor_masks) - 1: + odor_i = 0 + print(f"New odor index: {odor_masks[odor_i]}") + reply = device.send( + HarpMessage.WriteU16( + DelphiOnlyAppRegs.QueuedOdorMask, odor_masks[odor_i] + ).frame + ) + + """READ BASED ODOR UPDATING""" + if event_address == 32: + event_payload = msg.payload[0] + print(f"Valves State: {event_payload}") + # reply = device.send(HarpMessage.ReadU16(AppRegs.ValvesState).frame) + # if reply.payload[0] != 0: + # print(f"Valves State: {reply.payload[0]:16b}") + # if reply.payload[0] == -1: + # odor_i+=1 + # if odor_i > 3: + # odor_i = 0 + # print(f'New odor index: {odor_i}') + # reply = device.send(HarpMessage.WriteS8(DelphiOnlyAppRegs.QueuedOdorIndex, odor_i).frame) + +except KeyboardInterrupt: + print("Disabling FSM.") + reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.FSMEnabledState, 0).frame) + device.disconnect() diff --git a/software/pyharp/wait_for_pokes.py b/software/pyharp/wait_for_pokes.py new file mode 100755 index 0000000..bc9cb37 --- /dev/null +++ b/software/pyharp/wait_for_pokes.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +from pyharp.device import Device, DeviceMode +from pyharp.messages import HarpMessage +from pyharp.messages import MessageType +from app_registers import AppRegs, DelphiOnlyAppRegs +from struct import pack, unpack +from time import sleep +import os +import serial.tools.list_ports + +import logging +logger = logging.getLogger() +logger.addHandler(logging.StreamHandler()) + +# Open serial connection with the first Valve Controller. +com_port = None +ports = serial.tools.list_ports.comports() +for port, desc, hwid in sorted(ports): + if desc.startswith("delphi-controller"): + print("{}: {} [{}]".format(port, desc, hwid)) + com_port = port + break +device = Device(com_port) +device.info() # Display device's info on screen + +print() +print("Enabling all aux gpios as inputs.") +gpio_dir = 0b00000000 +reply = device.send(HarpMessage.WriteU8(AppRegs.AuxGPIODir, gpio_dir).frame) +print(f"reply: {reply.payload[0]:08b}") +print() +reply = device.send(HarpMessage.ReadU8(DelphiOnlyAppRegs.PokeDometer).frame) +print(f"Current pokedometer count is: {reply.payload}.") +print(f"Setting next odor.") +reply = device.send(HarpMessage.WriteS8(DelphiOnlyAppRegs.NextOdorIndex, 0).frame) +print(f"Assigning poke pin.") +reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.PokePin, 22).frame) +print(f"Inverting poke pin.") +reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.PokePinInverted, 1).frame) +print("Enabling FSM") +reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.FSMEnabledState, 1).frame) +print() + +try: + while True: + for msg in device.get_events(): + print(msg) + print() +except KeyboardInterrupt: + print("Disabling FSM.") + reply = device.send(HarpMessage.WriteU8(DelphiOnlyAppRegs.FSMEnabledState, 0).frame) + device.disconnect()