⚠️ ARCHIVED — June 2026. Active firmware development has moved into thereacherrepository underfirmware/. Compiled hex artifacts now live atreacher/src/reacher/hex/<board>/and ship inside thereacherPyPI package. This repository is read-only; open firmware issues and PRs againstOtis-Lab-MUSC/reacher. The hex files here remain downloadable for legacy builds but are no longer updated.Final release:
v2.1.0is the terminal release of this standalone repository.📖 Citing this firmware? The version referenced in the published protocol is
v1.0.1-alpha("Version for Protocol paper"). That release is preserved unchanged for reproducibility — cite it, notv2.1.0, when reproducing the paper.
Operant conditioning controller firmware for Arduino Mega 2560 (ATmega2560)
Written by: Joshua Boquiren
This repository contains Arduino C++ firmware implementing five behavioral paradigms for head-fixed rodent operant conditioning experiments. Each paradigm is compiled into a standalone .hex file that can be uploaded to an Arduino Mega 2560 via the REACHER frontend UI or manually via arduino-cli/avrdude.
Paradigms:
- Fixed Ratio (FR) — reward after N active lever presses
- Progressive Ratio (PR) — reward threshold increases by a configurable step after each delivery
- Variable Interval (VI) — reward available during a random window within each fixed interval
- Omission — reward delivered after the animal withholds pressing for a specified duration
- Pavlovian — classical conditioning with CS+/CS- trials, cues, and probabilistic reward delivery
The firmware is the embedded hardware layer. It directly controls all peripheral devices (levers, syringe pumps, tone cues, optogenetic lasers, lick detection circuits, and microscope synchronization triggers) and communicates with the Python backend over USB serial using JSON messages at 115200 baud.
Arduino Firmware ◄──USB Serial (115200 baud)──► Python Backend ◄──► React Frontend
(firmware/) JSON messages (server/) (web/)
The firmware receives configuration commands from the backend, executes behavioral paradigms autonomously, and streams all events (lever presses, pump activations, lick detections, frame timestamps) back in real time.
| Component | Description |
|---|---|
| Arduino Mega 2560 | ATmega2560 microcontroller |
| USB cable | Type-A to Type-B for serial connection |
| Levers | Two momentary switches (right-hand and left-hand) |
| Syringe pumps | Two relay-driven pumps (primary and secondary) |
| Tone speakers | Two speakers for auditory cues (PWM-driven) |
| Laser | Optogenetic laser module (PWM, up to 40 Hz square-wave) |
| Lick circuit | Capacitive or resistive lick detection circuit |
| Microscope | Two-photon microscope with TTL trigger input and frame timestamp output |
All pin assignments are defined in libraries/REACHERDevices/src/Pins.h:
| Pin | Constant | Direction | Description |
|---|---|---|---|
| 2 | PIN_MICROSCOPE_TS |
INPUT (INT0) | Microscope frame timestamp — rising-edge ISR captures frame times |
| 3 | PIN_CUE |
OUTPUT (PWM) | Primary tone cue speaker |
| 4 | PIN_PUMP |
OUTPUT | Primary syringe pump relay |
| 5 | PIN_LICK_CIRCUIT |
INPUT_PULLUP | Lick detection circuit |
| 6 | PIN_LASER |
OUTPUT (PWM) | Optogenetic laser |
| 7 | PIN_CUE_2 |
OUTPUT (PWM) | Secondary tone cue speaker |
| 8 | PIN_PUMP_2 |
OUTPUT | Secondary syringe pump relay |
| 9 | PIN_MICROSCOPE_TRIG |
OUTPUT | Microscope trigger pulse (50 ms HIGH) |
| 10 | PIN_LEVER_RH |
INPUT_PULLUP | Right-hand lever |
| 13 | PIN_LEVER_LH |
INPUT_PULLUP | Left-hand lever (note: shares the onboard LED line; INPUT_PULLUP validated but revisit if reads are unstable) |
Description: The animal must complete N active lever presses to earn a reward. Each reward delivery consists of a cue tone, a pump infusion, and an optional laser pulse, followed by a timeout period during which presses are logged but not reinforced.
Press classifications:
ACTIVE— press on the reinforced lever outside the timeout period; counted toward the ratioTIMEOUT— press during the post-reward timeout periodINACTIVE— press on the non-reinforced lever
Default configuration (fr/Config.h):
| Parameter | Default | Description |
|---|---|---|
| Ratio | 1 | Number of active presses required per reward |
| Cue frequency | 8000 Hz | Primary tone frequency |
| Cue duration | 1600 ms | Tone duration |
| Pump duration | 2000 ms | Infusion duration |
| Laser frequency | 40 Hz | Laser oscillation frequency |
| Laser duration | 5000 ms | Laser pulse duration |
| Timeout | 20000 ms | Post-reward timeout interval |
| Trace interval | 0 ms | Delay between cue offset and pump onset |
Reward chain: Cue (immediate) → Pump (after cue + trace) → Laser (after cue + trace) → Timeout (immediate)
Description: Like Fixed Ratio, but the press threshold increases by a configurable step after each reward delivery using arithmetic progression (e.g., 1, 2, 3, 4... with step=1).
Press classifications: Same as FR (ACTIVE, TIMEOUT, INACTIVE).
Additional configuration:
| Parameter | Default | Description |
|---|---|---|
| PR step | 1 | Arithmetic increment added to the threshold after each reward |
All other defaults are the same as FR. The threshold increases as: threshold += step after each reward.
Description: A fixed-length interval contains a randomly placed availability window. A lever press during the availability window triggers the reward. Presses outside the window are logged but not reinforced.
Press classifications: Same as FR (ACTIVE, TIMEOUT, INACTIVE).
Additional configuration:
| Parameter | Default | Description |
|---|---|---|
| VI interval | 15000 ms | Total interval length (window is placed randomly within it) |
The availability window start and end positions are sampled uniformly within each interval. After a reward or interval expiry, a new interval begins with a new random window.
Description: The animal is rewarded for withholding lever presses. If no active press occurs for the specified duration, the reward fires. Any active press resets the absence timer. There is no timeout period.
Press classifications:
ACTIVE— press on the reinforced lever; resets the absence timerINACTIVE— press on the non-reinforced lever
Configuration:
| Parameter | Default | Description |
|---|---|---|
| Omission interval | 20000 ms | Duration of press absence required to trigger reward |
| Cue frequency | 8000 Hz | Primary tone frequency |
| Cue duration | 1600 ms | Tone duration |
| Pump duration | 2000 ms | Infusion duration |
| Laser frequency | 40 Hz | Laser oscillation frequency |
| Laser duration | 5000 ms | Laser pulse duration |
| Timeout | 0 ms | No timeout in omission paradigm |
Reward chain: Cue (immediate) → Pump (immediate) → Laser (immediate)
Description: A trial-based paradigm with CS+ and CS- trials. CS+ trials present a continuous tone followed by reward delivery. CS- trials present a pulsed tone with no reward (by default). Trial order is randomized using Fisher-Yates shuffle with a constraint of no more than 3 consecutive same-type trials. Inter-trial intervals (ITI) are sampled from a clamped exponential distribution.
This paradigm uses a dedicated PavlovianScheduler state machine (not the shared Scheduler engine) with five phases: IDLE → ITI → CUE_ON → TRACE → REWARD.
Lever behavior: Both levers are set as reinforced. Presses are logged as ACTIVE but do not affect trial progression or reward delivery.
No laser device is used in the Pavlovian paradigm.
Default configuration (pavlovian/pavlovian.ino):
| Parameter | Default | Command Code | Description |
|---|---|---|---|
| CS+ count | 50 | 208 | Number of CS+ trials |
| CS- count | 50 | 209 | Number of CS- trials |
| CS+ frequency | 12000 Hz | 210 | Tone frequency for CS+ |
| CS- frequency | 3000 Hz | 211 | Tone frequency for CS- |
| CS+ reward probability | 100% | 206 | Probability of reward on CS+ trials |
| CS- reward probability | 0% | 207 | Probability of reward on CS- trials |
| Counterbalance | false | 212 | Swap CS+ and CS- cue assignments |
| Cue duration | 2000 ms | 213 | Duration of conditioned stimulus |
| Trace interval | 1000 ms | 214 | Delay between CS offset and reward |
| Consumption period | 3000 ms | 215 | Time allowed for reward consumption |
| ITI mean | 30000 ms | 216 | Mean inter-trial interval |
| ITI min | 10000 ms | 217 | Minimum ITI |
| ITI max | 90000 ms | 218 | Maximum ITI |
| Pulse config | 200/200 ms | 219 | CS- pulsed tone ON/OFF durations |
All paradigms share a common device library at libraries/REACHERDevices/ (v2.0.0, avr architecture).
Device (base)
├── SwitchLever — Lever monitoring with debounce, press classification, timeout tracking
├── Cue — Tone generation via Arduino tone(), continuous or pulsed modes
├── Pump — Relay-driven syringe pump activation
├── Laser — PWM laser with contingent/independent modes, square-wave oscillation
└── LickCircuit — Lick detection with debounce and event logging
Microscope (standalone) — Frame timestamp capture via INT0 ISR + trigger pulse output
Scheduler — Contingency engine for operant paradigms (FR, PR, VI, Omission)
├── Trigger — Event sources: PRESS_COUNT, ABSENCE_TIMER, AVAILABILITY_WINDOW, MANUAL
├── Chain — Ordered sequence of Actions (up to 6 steps)
├── Action — ACTIVATE_DEVICE, SET_TIMEOUT, RESET_TRIGGER, NONE
└── PendingAction — Time-delayed action execution queue (up to 8 pending)
PavlovianScheduler — Trial-based state machine for classical conditioning (standalone)
DeviceSet — Struct grouping all device pointers for helper functions
Device (base class) — Device.h
- Constructor:
Device(int8_t pin, uint8_t mode, const char* device) - Common interface:
ArmToggle(bool),SetOffset(uint32_t),Pin(),Armed()
SwitchLever — 20 ms debounce, press/release callbacks, active/inactive/timeout classification
Cue — Uses Arduino tone() (Timer2). Supports continuous and pulsed modes (for CS- in Pavlovian)
Pump — Drives relay HIGH for the configured infusion duration
Laser — Two modes: CONTINGENT (fires only via chain trigger) or INDEPENDENT (free-running square-wave cycle). Frequency of 1 Hz = continuous ON; >1 Hz = oscillation
LickCircuit — 20 ms debounce, logs lick events with start/end timestamps
Microscope — Manages two pins: trigger output (50 ms HIGH pulse) and INT0 ISR for frame timestamp capture
The Scheduler class implements a trigger-chain-action system for the four operant paradigms:
Trigger types:
| Type | Paradigm | Description |
|---|---|---|
PRESS_COUNT |
FR, PR | Fire chain after N active presses |
ABSENCE_TIMER |
Omission | Fire chain after N ms with no active press |
AVAILABILITY_WINDOW |
VI | Fire chain if pressed during random window |
MANUAL |
All | Fire chain via serial test command |
Action types:
| Type | Description |
|---|---|
ACTIVATE_DEVICE |
Call device->Activate() with offset delay |
SET_TIMEOUT |
Set lever timeout end time |
RESET_TRIGGER |
Reset trigger press count to zero |
NONE |
No-op placeholder |
| Class | Description |
|---|---|
ACTIVE |
Press on reinforced lever outside timeout — counted toward trigger |
INACTIVE |
Press on non-reinforced lever |
TIMEOUT |
Press during post-reward timeout period |
| Parameter | Value |
|---|---|
| Baud rate | 115200 bps |
| Timeout | 10 ms |
| Format | Newline-delimited JSON (CR-LF) |
| Identification | *IDN? query → JSON response with sketch, version, baud rate |
| Level | Meaning |
|---|---|
000 |
Configuration dump (firmware identification, device settings) |
001 |
State changes (arm/disarm notifications) |
006 |
Error messages |
007 |
Behavioral events (presses, infusions, licks, device activations) |
008 |
Microscope frame timestamps |
{
"level": "000",
"device": "CONTROLLER",
"sketch": "fr.ino",
"version": "v2.0.0",
"baud_rate": 115200,
"schedule": "FIXED_RATIO"
}| Range | Target | Key Codes |
|---|---|---|
| 100–105 | Controller | SESSION_END (100), SESSION_START (101), IDENTIFY (102), TEST_CHAIN (103), TEST_MODE (104), SESSION_PAUSE (105) |
| 201–220 | Session setup | SET_RATIO (201), SET_PARADIGM (202), SET_OMISSION_INTERVAL (203), SET_VI_INTERVAL (204), SET_PR_STEP (205), Pavlovian params (206–219), SET_TRACE_INTERVAL (220) |
| 300–382 | Cue | Primary: ARM (301), DISARM (300), TEST (303), FREQ (371), DUR (372). Secondary: ARM (311), DISARM (310), TEST (313), FREQ (381), DUR (382) |
| 400–482 | Pump | Primary: ARM (401), DISARM (400), TEST (403), DUR (472). Secondary: ARM (411), DISARM (410), TEST (413), DUR (482) |
| 500–501 | Lick circuit | DISARM (500), ARM (501) |
| 600–682 | Laser | ARM (601), DISARM (600), TEST (603), FREQ (671), DUR (672), CONTINGENT (681), INDEPENDENT (682) |
| 900–903 | Microscope | DISARM (900), ARM (901), TEST (903) |
| 1000–1081 | Right lever | ARM (1001), DISARM (1000), TIMEOUT (1074), RATIO (1075), INACTIVE (1080), ACTIVE (1081) |
| 1300–1381 | Left lever | ARM (1301), DISARM (1300), TIMEOUT (1374), RATIO (1375), INACTIVE (1380), ACTIVE (1381) |
arduino-cliinstalled and on your PATH- Arduino AVR board package:
arduino-cli core install arduino:avr
cd reacher-firmware
./compile.shThe script compiles all five paradigms (fr, pr, vi, omission, pavlovian) using the local libraries/ directory and outputs .hex files to the hex/mega/ directory:
hex/mega/
├── fr.hex
├── pr.hex
├── vi.hex
├── omission.hex
└── pavlovian.hex
Target board: arduino:avr:mega:cpu=atmega2560 (Arduino Mega 2560, ATmega2560).
The easiest method. In the REACHER UI:
- Select a COM port and create a session
- Choose a paradigm from the dropdown
- Click "Upload Firmware"
The backend handles the upload via avrdude automatically.
arduino-cli upload -p /dev/ttyUSB0 --fqbn arduino:avr:mega:cpu=atmega2560 --input-file hex/mega/fr.hexavrdude -p atmega2560 -c wiring -P /dev/ttyUSB0 -b 115200 -U flash:w:hex/mega/fr.hex:i| Mechanism | Details |
|---|---|
| Debouncing | SwitchLever: 20 ms, LickCircuit: 20 ms |
| Interrupt-driven frame capture | Microscope frame timestamps captured via rising-edge INT0 ISR on pin 2 |
| Real-time serial logging | All events are sent immediately over serial with session-relative timestamps |
| JSON format | Structured data prevents parsing ambiguity |
reacher-firmware/
├── compile.sh # Compilation script for all 5 paradigms
├── Doxyfile # Doxygen configuration
├── fr/ # Fixed Ratio paradigm
│ ├── fr.ino
│ └── Config.h
├── pr/ # Progressive Ratio paradigm
│ ├── pr.ino
│ └── Config.h
├── vi/ # Variable Interval paradigm
│ ├── vi.ino
│ └── Config.h
├── omission/ # Omission paradigm
│ ├── omission.ino
│ └── Config.h
├── pavlovian/ # Classical conditioning paradigm
│ ├── pavlovian.ino
│ ├── PavlovianScheduler.h
│ └── PavlovianScheduler.cpp
├── libraries/
│ └── REACHERDevices/ # Shared device library (v2.0.0)
│ ├── library.properties
│ └── src/
│ ├── Pins.h # Pin assignments
│ ├── Commands.h # Serial command codes
│ ├── Device.h/.cpp # Base class
│ ├── SwitchLever.h/.cpp
│ ├── Cue.h/.cpp
│ ├── Pump.h/.cpp
│ ├── Laser.h/.cpp
│ ├── LickCircuit.h/.cpp
│ ├── Microscope.h/.cpp
│ ├── Scheduler.h/.cpp # Trigger/chain/action engine
│ ├── Trigger.h
│ ├── Action.h
│ └── ReacherHelpers.h/.cpp
├── hex/ # Compiled hex binaries
└── docs/ # Doxygen-generated documentation
Full Doxygen-generated class and function documentation is available in the docs/ directory. To regenerate:
doxygen DoxyfileSee the top-level README for instructions on building the complete standalone REACHER application, which bundles the firmware hex files, frontend, and backend into a single executable.
This project is licensed under the MIT License.
Joshua Boquiren — thejoshbq@proton.me