-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathOdorChoice.ino
1615 lines (1532 loc) · 62.6 KB
/
OdorChoice.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Odor Choice Trial (C) Gavin Perry May 2024 - Dec 2024
// CHECK - This is V1 config
// Program to take serial input parameters from a Matlab program,
// Perform the requested trial while collecting data
// Return real time data to the Matlab program, no Report, let MatLab handle that
// Wait for the next trial
// Passing parameters without spaces or linefeeds is fine
// Example: (defines vars with default values, then starts trial )
// default: I2500O1P1A0B1D70T2400W2000U70S1G
// other: I2100O2P3A3B4D4T2300W2600U65G
//
// Immediate commands
// a b c d - Give reward now LA LB RA RB
// n play note (test)
// r (DEBUG only report parameters)
// s Resync to microscope
// v Open Valve
// x STOP! Pause at end of trial
// z Buzzer
// Parameter Codes from Matlab used (details in ReadInputs)
// com handshake i d (During setup)
// A B C D E F G H I M N O P S T U X W Y Z
// Available letters K Q R V
//
// Codes to MatLab
// #1 ID needs reply, #2 ID running needs no reply
// E F L O R S s U V W X x Y Z z (S start is ITI start)
// x is stopping a trial
// Moved older issue tracking to GitHub (ssh) [email protected]:Gavin-Perry/OdorChoice.git
// Version 28 -- aligning Pico code program numbers with ML code
// V29 aligning with ML
// Temporary difference between left and right for when to allow more licks
// V30 faster RewEvery lick rate, clean up burst counting
// Stable version
// V31 start 10/12
// V33 fixed GivReward for faster drops when manual as well as lick every
// V40 do v33 idea with ManRew variable
// V41 fix Air VAC order and CloseAllValves order (L then R)
// v42 for V1 hardware
// v43 various fixes
// v44 add New Training mode
// v45 Add Separate drop times
// v46 add restart of core 1 wit x2 and various bug fixes
// Display version on boot
#define vnum 48
//=================================
// TO DO
// Simulation Test parts of code here https://wokwi.com/projects/405222044520961025
// or make a new one here https://wokwi.com/projects/new/pi-pico
//
#include "pico/stdlib.h"
#include "pico/multicore.h"
/* Some pico specific functions that may be useful
// void rp2040.reboot() // Forces a hardware reboot of the Pico. (Use on error? Watchdog TO?)
// void rp2040.idleOtherCore() // pause the other core until..
// void rp2040.resumeOtherCore()
// void rp2040.restartCore1() // Hard resets Core1 from Core 0 and restarts its operation from setup1().
*/
// IF NO HEARTBEAT AT START, PROBABLY NO SERIAL COM CONNECTED!!!
// CLOSE THEN REOPEN THE SERIAL MONITOR IF DEBUGGING
//================================ ATTENTION!!=========================
// loop (0) to do most of USB communication (Core zero is connected to USB hardware)
// loop 1 does the trials and some quick com. (Passed to Core0)
//====================================================================================
// #include <Libraries> use "MyLib.h" if any headers in the local dir
#include <ShiftRegister74HC595.h> // Shifter lib
// #include <Arduino.h> // in the above lib
#define PCBv1 // Work around code for version 1 PCB AND for PCBv2 (v2C not built)
// #define VAOrder // Fix Vac/Air pin assignment order Can be done for V1 or V2 with wiring fix at valves
// Debugging levels set here
#define DEBUG // if defined, debug code is compiled in -- Comment out for no debug code
// #define DEBUG2 // verbose, mostly for testing handshake to ML
// Define default values for any params that need them for user convenience
#define dfCWT 2500 // default Choice Wait Time (W)
#define dfITI 2000 // (I)
#define dfMinNumLicks 1 // set with param (L) Keep small for debugging
#define dfMxLkTm 500 // Maximum time between licks to stay in burst
// v45 separate drop times for each valve
#define dfDropTmA 5010 // usec set by param (U) v43
#define dfDropTmB 5020 // usec set by param (U) v43
#define dfDropTmC 5030 // usec set by param (U) v43
#define dfDropTmD 5040 // usec set by param (U) v43
#define dfDropDelay 150 // InterDrop Interval
#define dfRewEvSpaceTm 20 // Delay before next freebie lick with RewEvery
#define dfOdorTm 2000 // ms of odor on (T)
#define dfSyncPol 1 // Low to high (S)
#define dfToneTm 200 // Set default tone (Note) duration (Y)
#define dfBuzzTm 1000 // how long for error buzz sound (Z)
#define dfMaxDrops 7 // default maximum # of drops thus sets total reward time
// Constant values that have to change here
// no variable for live changes unless requested??
#define SynPulseTm 10 // Sync out to microscope duration (fixed but can change)
#define MinLickInterval 20 // minimum ms between licks FIXED
#define MaxLickList 100 // end of Lick lists, 2 lists, one each Left and Right
#define PostSync 2000 // Time to wait after sync out before start of trial
#define EndSync 1000 // wait at end of trial brefore giving the sync (mscope off)??
// Set notes for error and go tones. Mice hear 1kHz to >80kHz
// Use sounds in human hearing range too. Notes set to minimize dissonance for user
#define Buzz 2489 // D7 near resonance of piezo speaker
#define Note 1865 // A#6 mice hear above 1kHz, Want this higher than buzz or lower OK ??
#define ClickTm 10 // ms for extra click to be sure valves close
//----------------- Pin map using GPIO#s not pin#s ----------------------------
// GP 0-9 for left, expansion sr. 1-10 for right
#define OL1 0 // Not changed with V2 board
#define OL2 1 //
#define OL3 2 //
#define OL4 3 //
#define OL5 4 //
#define OL6 5 //
#define OL7 6 //
#define OL8 7 //
#define OL9 8 //
#define OL10 9 // Last odor Left - direct pins
// #define OR1 // Shift reg output1 (skipped zero)
#ifdef VAOrder
#define AirL 10 // (Odor 0 Left)
#define AirR 11 // (Odor 0 Right)
#define VAC 12 //
#else
#define VAC 10 //
#define AirL 11 // (Odor 0 Left)
#define AirR 12 // (Odor 0 Right)
#endif
// Right valves use '595 expansion chip with ShiftReg lib included
// Name GP# pin# pico pin#
const int dataPin = 13; // 595 p14 pico p17
const int clockPin = 14; // 595 p11 pico p19
const int latchPin = 15; // 595 p12 pico p20
// create 16 bit shift reg with 2 'HC595nICs in series
ShiftRegister74HC595<2> sr(dataPin, clockPin, latchPin); // create a shift register pair <2>
// The right valves are mapped into the '595 shifter pins (SR1 to SR10)
// Reserved/Spare: SR0, SR11-SR15
#define SyncIn 16 // TTL in from Microscope (Uses OptoIso) Gives a timestamp at start of pulse
#define SyncOut 17 // TTT to Microscope (Opto?)
// 4 reward solenoids
#define RewLeftA 18 // Pin 24
#define RewLeftB 19 // pin 25
#define RewRightA 20 // pin 26
#define RewRightB 21 // pin 27
#define BUZZER_PIN 22 // pin 29
// SMPS Powersave 23 // dedicated pins
// VBUS in sense 24
// LED_BUILTIN 25
// Lick detect inputs ref to AGND pin 33 for lower noise?
#define LickLeft 26 // input ADC0
#define LickRight 27 // input ADC1
// Comm chars from MatLab
#define idChar 'i'
#define ackChar 'd'
// (ID code to matlab #1 )
//----------------------------------------------------------------------------------------
// Variables
// Event timing
int OdorTm = dfOdorTm; // Set to defaults defined above 'T'
int DropTmA = dfDropTmA;
int DropTmB = dfDropTmB;
int DropTmC = dfDropTmC;
int DropTmD = dfDropTmD;
int DropDelay = dfDropDelay;
int svDropDelay = DropDelay; // save value since New training messes with it
int MinDD = 50; // fastest possible time to next reward (ms) in RewEvery case
int ITI = dfITI; // Wait time before trial
unsigned long // milli() Times
RunTime, // Loop (0) time check
StartTm,
EndTime, // End of current trial
EndTm, // End of CWT
RewEvSpaceTm = dfRewEvSpaceTm, // Minimum time between free Rewards
HBeat; // Tick Builtin LED in loop 1
// debugging defaults:
byte OdorL = 1, // "O" value Which Odor on the left 1-5 is A
OdorR = 6, // "P" value 6-10 is B
RewLA = 1, // Rew Left A number of drops
RewLB = 0, // Rew Left B
RewRA = 0, // Rew Right A
RewRB = 1; // Rew Right B
int ToneTm = dfToneTm; // Set default tone (Note) duration
int BuzzTm = dfBuzzTm; // how long for error buzz sound
int CWT = dfCWT; // Choice Wait Time
char Cmd = 0; // Make command global so other routines can check it
// For bools, 1= true, yes, on; 0= false no off
bool isMatLabPresent = false;
bool NoCountErr = false; // Not counting errors
// Is Error only needed for debugging - Give Error code ASAP
bool IsDone = false; // Trial is complete (escape CWT)
bool RewEvery = false; // Reward Every Lick
bool RewEvA = true; // for RewEvery true: the juice A, false B
bool NewLickLeft = false; // Unreported lick on the left?
bool NewLickRight = false; // Unreported lick on the right?
bool IsFirstLick = true; // No licks yet, first of burst
bool WasLastLickLeft = false; // true if the last lick was on the left
bool SyncPol = dfSyncPol; // Sync polarity
bool Stress = false; // running stress test
bool ManRew = false; // Manual Reward delivered, skip delays
// Trial managment flags to communicate between processors
// The goal with so many vars is to allow some overlap
// Probably a waste of time and can be simplified ???
// e.g. reporting data from last trial while ITI can start in the next
// VERIFY Did I get this right?
bool GoTime = false; // It's time to start a trial
bool TrialRunning = false; // true while a trial is running, to the end
// Lick stuff
int MxLkTm = dfMxLkTm;
unsigned long NewLickLeftTm = 0; // Save time of most recent L lick in ISR, process in CheckNow
unsigned long NewLickRightTm = 0; // Save time of most recent R lick in ISR, process in CheckNow
unsigned long LastLickLeftTm = 0; // Use to check Minimum spacing between licks
unsigned long LastLickRightTm = 0; // Use to check Minimum spacing between licks
int LickCountL = 0; // Counting licks for reward
int LickCountR = 0;
int MinNumLicks = dfMinNumLicks; // Minimum Licks for reward 'L'
int MaxDrops = dfMaxDrops;
int NewTrainCnt = 0; // New Training mode, value is count for each side for alternating
bool first = true; // for hold off loop1 until loop is running
// Declare procs
bool CheckID();
void CheckNow();
void ToggleLED();
bool ReadInput(); // Parse inputs from MatLab, set Cmd value, handle immediate commands
bool ReadCmds(char Cmd); // Parse and execute Trial commands
void LowerCaseCmd(char Cmd); // Called in ReadInput and on error in ReadCmds with lc letter
bool ExecuteTrial(); // True if successful false on timeout or other failure
void OpenAirVac();
void CloseAirVac();
void OpenOdorValves(signed char Lft, signed char Rt);
void CloseOdorValves(char Lft, char Rt);
void CloseAllValves();
void DoSyncPulse();
void ChkLeft(); // if burst of left licks process response
void ChkRight(); // if burst of right licks process response
void GiveReward(byte RewLoc, int Drops); // RewPin, # of drops (RewLA RewLB RewRA or RewRB)
void LickLISR();
void LickRISR();
void SyncInISR();
void ErrorBuzz(int ErrNum);
int isAABB(); // New 10/3 for checking AA (1) or BB (2) choices AB==0
void DoNewTrain(); // New Training (alt sides Cnt each)
//
void setup() {
unsigned long runTime = millis(); // Use a local version for timing in setup
pinMode(LED_BUILTIN, OUTPUT); // make sure LED_PIN is output
digitalWrite(LED_BUILTIN, HIGH);
pinMode(BUZZER_PIN, OUTPUT_12MA); // Output drive can be 2 4 8 or 12 mA
for (byte i = 0; i <= 22; i++) { // set all output pins on and LOW
pinMode(i, OUTPUT);
digitalWrite(i, LOW);
}
sr.setAllLow(); // set all ShiftReg pins LOW
pinMode(LickLeft, INPUT); // Licking inputs
pinMode(LickRight, INPUT);
pinMode(SyncIn, INPUT_PULLUP); // Sync input - opto will pull down when it's input is high
pinMode(SyncOut, OUTPUT); // Sync output
Serial.begin(57600); // Serial monitor (115200 is maximum rate but it doesn't really matter with USB)
while (!Serial) { // Wait for the serial port to connect
delay(100); // While waiting for Serial port to connect - fast(ish) toggle LED
ToggleLED();
}
Serial.flush(); // OK, we can talk to PC now
#ifdef DEBUG2
Serial.println("#Setup");
#endif
// Make sure a connection is established with MatLab Program- Do Handshake
isMatLabPresent = CheckID(); // first check for ID
//*/ This code runs when CheckID times out
if (!isMatLabPresent) { //
isMatLabPresent = CheckID(); // Retry again each CheckID is 10 sec
while (!isMatLabPresent) { // Still missing?
#ifdef DEBUG
Serial.println("#Missed CheckID ");
#endif
isMatLabPresent = CheckID(); // Retry for 20 seconds (each CheckID is 5 sec?)
// different flash code for this problem
if (digitalRead(LED_BUILTIN))
delay(400); // dash on, dot off
else delay(100);
ToggleLED(); // Toggle LED
if (millis() > (runTime + 20000)) {
if (!isMatLabPresent) // This took too long
Serial.println("#Help! COM failure"); // But no one is listening!
runTime = millis();
}
} // while no Matlab present - semi patient.
} else
#ifdef DEBUG2
Serial.println("#MatLab is Present");
#endif
//*/
// Check that pins can interrupt and attach them to proc0 (or proc1 in Setup1)
digitalWrite(LED_BUILTIN, HIGH);
delay(10); // Need delay before attaching ISRs?
if ((digitalPinToInterrupt(LickLeft) < 0) || (digitalPinToInterrupt(LickRight) < 0) || (digitalPinToInterrupt(SyncIn) < 0)) {
Serial.println("#Warning NOT an interrupt pin for lick or sync"); // Error message to be NEVER seen unless MPU is changed
} else {
attachInterrupt(digitalPinToInterrupt(LickLeft), LickLISR, FALLING); // Count licks
attachInterrupt(digitalPinToInterrupt(LickRight), LickRISR, FALLING);
attachInterrupt(digitalPinToInterrupt(SyncIn), SyncInISR, RISING); // _-_ microscope sync received
}
/*
digitalWrite(RewLeftA, HIGH); // test
digitalWrite(RewLeftB, HIGH);
digitalWrite(RewRightA, HIGH);
digitalWrite(RewRightB, HIGH);
*/
// test buzzer sounds (takes 500ms)
tone(BUZZER_PIN, Buzz, 150); // generates a 150ms beep note D5
delay(200); // tone is Non-blocking, so wait
tone(BUZZER_PIN, Note, 150); // generates a 100ms beep note A6
// Need this when MatLab runs it? (Should always give a value)
Serial.setTimeout(20); // Doing Serial.Available so only times out when no val sent
/*
digitalWrite(RewLeftA, LOW); // test
digitalWrite(RewLeftB, LOW);
digitalWrite(RewRightA, LOW);
digitalWrite(RewRightB, LOW);
*/
digitalWrite(LED_BUILTIN, LOW); // Setup complete
#ifdef DEBUG2
delay(100);
Serial.print("#Setup done at ");
Serial.println(millis());
#endif
} // end setup()
// Start second processor
void setup1() { // nothing needed to set up loop1??
// Init some default variables for re-boot
ITI = dfITI;
CWT = dfCWT;
OdorTm = dfOdorTm;
DropTmA = dfDropTmA;
DropTmB = dfDropTmB;
DropTmC = dfDropTmC;
DropTmD = dfDropTmD;
DropDelay = dfDropDelay;
MxLkTm = dfMxLkTm;
LickCountL = 0; // Counting licks for reward
LickCountR = 0;
MinNumLicks = dfMinNumLicks; // Minimum Licks for reward
// reset vals for watchdog restart
TrialRunning = false;
GoTime = false;
}
//*****************************************
// Loop for first processor, Watch for commands and set params
// does the heartbeat when trial is NOT running
// handle Serial IO with Matlab, that's a USB function)
void loop() { // Need this loop fast as it process completion routines for licks
CheckNow(); // Look for commands and interrupts (licks) completed
// CheckNow calls ReadInput which will (via ReadCmds) set GoTime
// Check the side effects (from other processor) as to what to do next here
// if it's time to do a trial. loop() does the trials
if (NewTrainCnt == 0) { // v45 no HB during NewTraining
// Heartbeat between trials (during trial should be steady on)
if ((millis() - HBeat) > 1000) { //HBeat rate one sec on, one off
if (!GoTime && !TrialRunning) {
HBeat = millis();
ToggleLED(); // Toggle it
}
}
}
first = false; // Quicker to just do it without checking
}
// end loop
//==================Loop1 stops heartbeat, runs the trials ======================
void loop1() {
while (first) { // Waiting for Proc0 to do one loop
delay(10);
}
//main code to run the trials
if (GoTime && !TrialRunning) { // Wait for it if there is already a trial running
// Probably towards the end so catch it in a future loop
digitalWrite(LED_BUILTIN, HIGH); // LED on during a trial
ExecuteTrial(); // Can Pay attention to Error trials??? (returns false)
digitalWrite(LED_BUILTIN, LOW); // Trial finished, LED off
}
// Not running a trial? How about Training? v44
if (NewTrainCnt > 0) {
svDropDelay = DropDelay; // save value
DoNewTrain();
DropDelay = svDropDelay; // Restore
}
// Do some more error checking??? of what?
} // end loop1
//======================================================
void ToggleLED() {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // Toggle LED
}
// ---------------------- for setup() and loop() ----------------
// Wait for a char from ML, read it, if it's idChar reply #1, wait for ackChar
// Keep checking for 10 seconds before complaining.
// We know there is a serial connection so this shouldn't be needed more than once
bool CheckID() {
char inByte = ' '; // empty not allowed, use space (or zero / null)
unsigned long timeout;
// in case lost sync or matLab restarted while Pico running
// Should I handle other async cases?
if (isMatLabPresent) {
return true; // No need to check again
}
timeout = millis();
do {
ToggleLED(); // Toggle LED
delay(40); // Fast LED flicker while waiting
if (Serial.available() > 0) { // keep reading input
inByte = Serial.read();
if (inByte == idChar) {
Serial.println("#1"); // Announce to Matlab we are here
}
}
} while (inByte != ackChar); // Waiting for the 'd' from ML
#ifdef DEBUG
Serial.printf("#Pico Connected v%u\r\n", vnum); // tell of success
#endif
Serial.flush(); // getrid of any extra chars in buffer
return true;
} // end CheckID
//----------------------- Running Trial routines for Loop1() ---------------------------------
bool ExecuteTrial() { // MatLab RunTrial
IsFirstLick = true; // No licks yet
GoTime = false; // Started
TrialRunning = true;
#ifdef DEBUG
Serial.println("#GO");
#endif
StartTm = millis(); // Start of this trial. Does anyone care later?
Serial.printf("%c%u\r\n", 'S', StartTm);
if (ITI > 0) { // V43 skip air for training
delay(5); // 5ms to separate S from U
OpenAirVac(); // Clear last odors
if ((ITI - PostSync) > 0) { // if ITI < 2 sec, skip (v42 all this)
delay(ITI - PostSync); // wait out the ITI time (minus 2 sec)
DoSyncPulse();
Serial.printf("Y%u\r\n", millis()); // tell Sync
delay(PostSync); // wait for microscope to start
} else { // short delay for training v42
delay(ITI);
}
CloseAirVac();
}
if (OdorTm > 0) {
// Open odor valves
delay(10); // 10ms to separate Valves
OpenOdorValves(OdorL, OdorR);
// Checking for licks is done by interrupt
delay(OdorTm);
// Close odor valves
CloseOdorValves(OdorL, OdorR);
} // Air and odor stuff that is skipped in early training v42
// Announce licking time start
tone(BUZZER_PIN, Note, ToneTm); // Go time
delay(ToneTm); // Tone is async so have to wait
// What about licks of Air (Errors?) during or before Odors
IsDone = false; // A fresh start at CWT
Serial.printf("%c%u\r\n", 'W', millis());
#ifdef DEBUG
Serial.printf("#CWT %u\r\n", millis());
#endif
// check if licks added to list since odors presented
// Do any count as errors?
LickCountL = 0; // re-start count now
LickCountR = 0;
EndTm = millis(); // End of tone, count licks EndTm needed (for debug only?)
// Lick counting is done in CheckNow on the other processor that has the interrupts
while ((millis() - EndTm) < CWT) {
// Count Licks during CWT with LickCountL and LickCountR updates from CheckNow()
// Enough Licks to count as a decision? (MinNumLicks)
// Check if a reward should be given or an error counted
//======================== Left Lick burst? ====================================
if (LickCountL >= MinNumLicks) { // enough licks on Left to classify as choice
ChkLeft();
// after a good burst always reset the lick counts
LickCountL = 0; // Reset licks
}
if (IsDone) // v49 don't check right if done
break;
// ========================== Now check right side ==========================
if (LickCountR >= MinNumLicks) { // enough licks on Right to classify as choice
ChkRight();
LickCountR = 0; // Reset licks
}
if (IsDone) {
break;
}
} // end of while CWT
#ifdef DEBUG
Serial.printf("#End CWT %u\r\n", millis());
#endif
if (!IsDone) {
// If timed out it's an ignored trial
// No reward or error is ignored trial X -> (-1)
Serial.printf("%c%u\r\n", 'X', millis()); // Report trial ignored, and time
}
if (ITI > 0) {
delay(EndSync); // shorter time than before trial, but some wait before m'scope off??
DoSyncPulse();
}
Serial.printf("%c%u\r\n", 'Z', millis()); // Sync for microscope off report
delay(20); // Why not give a bit of time after the pulse (Is microscope going to pulse again?)
TrialRunning = false; // Pico knows Trial is finished
Serial.printf("%c%u\r\n", 'z', millis()); // Tell Matlab to save data and go to next trial.
return true; //
} // True if successful, possible False on timeout or other failure, not implemented that way
// end ExecuteTrial
//=================================================
int isAABB() { // Check if it's an AA, A0, BB, or B0 trial
if (OdorL < 6 && OdorR < 6) { // it's AA or A0
return 1;
} else if (((OdorL > 5) || (OdorL == 0)) && // BB or B0
((OdorR > 5) || (OdorR == 0))) {
return 2;
} else {
return 0; // false, normal AB trial
}
}
void ChkLeft() {
if (OdorL == 0) { // wrong choice Air on Left
if (NoCountErr) { // Keep going for another chance
#ifdef DEBUG2
Serial.printf("#NCErr L Air %u\r\n", millis());
#endif
} else { // An error, that counts as a fail
IsDone = true;
ErrorBuzz(1); // Error Left on Air
}
} else { // Not 0 odor; Good choice? check for larger on AA and BB offers
switch (isAABB()) { // What trial type
case 0: // Normal AB choice is OK
if (RewLA > 0) // Give A if that's the one
GiveReward(RewLeftA, RewLA); //
else GiveReward(RewLeftB, RewLB); // OK give B
IsDone = true;
break;
case 1: // AA trial, chose L
if (RewLA > RewRA) { // L more drops
GiveReward(RewLeftA, RewLA); //
IsDone = true;
} else { // oops
if (NoCountErr) {
#ifdef DEBUG
Serial.printf("#NCErr LA< %u\r\n", millis());
#endif
} else {
IsDone = true;
ErrorBuzz(2); // Fail
}
} // error
break;
case 2: // BB trial Chose R
if (RewLB > RewRB) { // More L drops = stronger odor
GiveReward(RewLeftB, RewLB); //
IsDone = true;
} else {
if (NoCountErr) {
#ifdef DEBUG
Serial.printf("#NCErr LB< %u\r\n", millis());
#endif
} else {
IsDone = true;
ErrorBuzz(3); // BB smaller picked
}
} // error
} // End switch
} // end Odor !0
} // Chk Left
void ChkRight() {
if (OdorR == 0) { // wrong choice it's Air
if (NoCountErr) { // and keep going
#ifdef DEBUG2
Serial.printf("#NCErr R Air %u\r\n", millis());
#endif
} else { // An error that counts
IsDone = true;
ErrorBuzz(4); // Error Right on Air
}
} else { // Not Air trial
switch (isAABB()) { // What trial type
case 0: // Normal AB
if (RewRA > 0) // Give A if that's the one
GiveReward(RewRightA, RewRA); //
else GiveReward(RewRightB, RewRB); // OK give B
IsDone = true;
break;
case 1: // AA trial, chose R
// Which is it if it matters ????
if (RewRA > RewLA) { // R more
GiveReward(RewRightA, RewRA); //
IsDone = true;
} else {
if (NoCountErr) {
#ifdef DEBUG
Serial.printf("#NCErr RA< %u\r\n", millis());
#endif
} else {
IsDone = true;
ErrorBuzz(5); //
}
} // error
break; // from switch
case 2: // BBB trial Chose R
if (RewRB > RewLB) {
GiveReward(RewRightB, RewRB); //
IsDone = true;
} else {
if (NoCountErr) {
#ifdef DEBUG
Serial.printf("#NCErr RB< %u\r\n", millis());
#endif
} else {
IsDone = true;
ErrorBuzz(6); //
}
} // error
} // End switch
} // else no Air
} // Check Right
void DoSyncPulse() { // Tell about it
digitalWrite(SyncOut, SyncPol);
delay(SynPulseTm);
digitalWrite(SyncOut, !SyncPol);
}
void OpenAirVac() {
digitalWrite(AirL, HIGH);
digitalWrite(AirR, HIGH);
digitalWrite(VAC, HIGH);
Serial.printf("%c%u\r\n", 'U', millis()); // U open; V close
}
void CloseAirVac() {
// 10/1 added extra clicks at end of close to be sure valves close
// v41 add time between clicks for less current pulses
digitalWrite(AirL, LOW);
delay(ClickTm);
digitalWrite(AirR, LOW);
delay(ClickTm);
digitalWrite(VAC, LOW);
delay(ClickTm);
digitalWrite(AirL, HIGH);
delay(ClickTm);
digitalWrite(AirR, HIGH);
delay(ClickTm);
digitalWrite(AirL, LOW);
delay(ClickTm);
digitalWrite(AirR, LOW);
delay(ClickTm);
digitalWrite(VAC, HIGH);
delay(ClickTm);
digitalWrite(VAC, LOW);
Serial.printf("%c%u\r\n", 'V', millis()); //
}
void OpenOdorValves(signed char Lft, signed char Rt) {
if (Rt >= 0) { // skip right if -, it's a manual test
if (Rt == 0)
digitalWrite(AirR, HIGH);
else {
// Open Right mapped to '595
// work around wiring bug in board PCB v1.0 AND v2.0 :-(
#ifdef PCBv1
if (Rt < 8)
sr.set(Rt, HIGH); // Shifter calls the bits 0-7 we skipped 0 pin
else
sr.set(Rt + 1, HIGH); // Missed QA on the right side of chip2
#else // PCB V2 also has bad wires
sr.set(Rt, HIGH); // PCB v2C QA corrected as intended
#endif
}
}
if (Lft >= 0) { // skip left if -, it's a manual test
if (Lft == 0)
digitalWrite(AirL, HIGH);
else {
// Open left valve map 1-10 to GP0-GP9 Version 10
digitalWrite(Lft - 1, HIGH);
}
}
if ((Lft >= 0) && (Rt >= 0))
Serial.printf("%c%u\r\n", 'N', millis()); // change 'O' to 'N' ver 43
else // valve testing, no time needed
Serial.printf("N\r\n", 'N', millis()); // change 'O' to 'N' ver 43
}
void CloseOdorValves(char Lft, char Rt) {
Serial.printf("%c%u\r\n", 'F', millis()); // Odors off
if (Rt >= 0) { // skip right if -, it's a manual test
if (Rt == 0) { //
digitalWrite(AirR, LOW);
delay(ClickTm);
digitalWrite(AirR, HIGH);
delay(ClickTm);
digitalWrite(AirR, LOW);
} else {
#ifdef PCBv1 // work around wiring bug in boards v1.0 and v2.0
if (Rt < 8) {
sr.set(Rt, LOW); // Shifter calls the bits 0-7 we skipped 0 pin
delay(ClickTm);
sr.set(Rt, HIGH); // Shifter calls the bits 0-7 we skipped 0 pin
delay(ClickTm);
sr.set(Rt, LOW); // Shifter calls the bits 0-7 we skipped 0 pin
} else {
sr.set(Rt + 1, LOW); // Missed QA on the right side of chip2 or bad wiring in V2
delay(ClickTm);
sr.set(Rt + 1, HIGH); // Shifter calls the bits 0-7 we skipped 0 pin
delay(ClickTm);
sr.set(Rt + 1, LOW); // Shifter calls the bits 0-7 we skipped 0 pin
}
#else
sr.set(Rt, LOW); // PCB v2C QA corrected (but not built)
delay(ClickTm);
sr.set(Rt, HIGH); //
delay(ClickTm);
sr.set(Rt, LOW); //
#endif
}
}
if (Lft >= 0) { // skip right if -, it's a manual test
if (Lft == 0) {
digitalWrite(AirL, LOW);
delay(ClickTm);
digitalWrite(AirL, HIGH);
delay(ClickTm);
digitalWrite(AirL, LOW);
} else {
digitalWrite(Lft - 1, LOW); // GP0 - 9 Version 10
delay(ClickTm);
digitalWrite(Lft - 1, HIGH); // GP0 - 9 Version 10
delay(ClickTm);
digitalWrite(Lft - 1, LOW); // GP0 - 9 Version 10
}
}
} // End Close Odor Valves
void CloseAllValves() {
for (int Lft = 1; Lft < 11; Lft++) {
digitalWrite(Lft - 1, LOW); // GP0 - 9 Version 10
delay(ClickTm);
digitalWrite(Lft - 1, HIGH); // GP0 - 9 Version 10
delay(ClickTm);
digitalWrite(Lft - 1, LOW); // GP0 - 9 Version 10
}
for (char Rt = 1; Rt < 11; Rt++)
#ifdef PCBv1 // work around wiring bug in boards 1.0
if (Rt < 8) {
sr.set(Rt, LOW); // Shifter calls the bits 0-7 we skipped 0 pin
delay(ClickTm);
sr.set(Rt, HIGH); // Shifter calls the bits 0-7 we skipped 0 pin
delay(ClickTm);
sr.set(Rt, LOW); // Shifter calls the bits 0-7 we skipped 0 pin
} else {
sr.set(Rt + 1, LOW); // Missed QA on the right side of chip2
delay(ClickTm);
sr.set(Rt + 1, HIGH); // Shifter calls the bits 0-7 we skipped 0 pin
delay(ClickTm);
sr.set(Rt + 1, LOW); // Shifter calls the bits 0-7 we skipped 0 pin
}
#else
{
sr.set(Rt, LOW); // PCB v2 QA corrected
delay(ClickTm);
sr.set(Rt, HIGH); // Shifter calls the bits 0-7 we skipped 0 pin
delay(ClickTm);
sr.set(Rt, LOW); // Shifter calls the bits 0-7 we skipped 0 pin
}
#endif
CloseAirVac();
// Close Rews too v48
digitalWrite(RewLeftA, LOW);
digitalWrite(RewLeftB, LOW);
digitalWrite(RewRightA, LOW);
digitalWrite(RewRightB, LOW);
delay(ClickTm);
digitalWrite(RewLeftA, HIGH);
digitalWrite(RewLeftB, HIGH);
digitalWrite(RewRightA, HIGH);
digitalWrite(RewRightB, HIGH);
delay(ClickTm);
digitalWrite(RewLeftA, LOW);
digitalWrite(RewLeftB, LOW);
digitalWrite(RewRightA, LOW);
digitalWrite(RewRightB, LOW);
Serial.println("#All valves closed");
}
void ValveTest(int rate) { // click each valve at rate (0.1 sec units)
#ifdef DEBUG
Serial.println("#Valve Test");
#endif
int ratems = rate * 10; // Convert to msec
for (char i = 1; i < 11; i++) { // each of 10 valves 1 - 10
OpenOdorValves(i, i);
delay(ratems);
CloseOdorValves(i, i);
delay(ratems / 2);
Serial.printf("# V%d", i);
}
tone(BUZZER_PIN, 2000, 500);
delay(600);
OpenAirVac(); // All 3 together
Serial.println("Air/Vac");
delay(ratems);
CloseAirVac();
delay(ratems / 2);
RewVTest(rate);
} // end Valve Test
// Test just for reward valves 10/4 added stress test
// Version 10/1/24 has added a final pulse after the open time
// to be sure the valves close
void RewVTest(int rate) { // rate is really a period in ms
if (rate == 1000) { //Stress test
Serial.println("#Stress test");
Stress = true;
long count = 0;
while (Stress) { // Runs until k pressed
for (int i = 0; i < rate; i++) {
digitalWrite(RewLeftA, HIGH);
digitalWrite(RewLeftB, HIGH);
digitalWrite(RewRightA, HIGH);
digitalWrite(RewRightB, HIGH);
delay(ClickTm);
digitalWrite(RewLeftA, LOW);
digitalWrite(RewLeftB, LOW);
digitalWrite(RewRightA, LOW);
digitalWrite(RewRightB, LOW);
delay(ClickTm);
if (Serial.available() > 0) {
Stress = false;
Serial.println("#End Stress Test");
break;
}
} // end for
count++;
Serial.printf("#Total: %u,000\r\n", count);
delay(500);
} // while Stress
} else if (rate > 50) { // do all together so it won't take so long
Serial.println("#Cleaning");
digitalWrite(RewLeftA, HIGH);
digitalWrite(RewLeftB, HIGH);
digitalWrite(RewRightA, HIGH);
digitalWrite(RewRightB, HIGH);
delay(rate);
digitalWrite(RewLeftA, LOW);
digitalWrite(RewLeftB, LOW);
digitalWrite(RewRightA, LOW);
digitalWrite(RewRightB, LOW);
delay(ClickTm);
digitalWrite(RewLeftA, HIGH);
digitalWrite(RewLeftB, HIGH);
digitalWrite(RewRightA, HIGH);
digitalWrite(RewRightB, HIGH);
delay(ClickTm);
digitalWrite(RewLeftA, LOW);
digitalWrite(RewLeftB, LOW);
digitalWrite(RewRightA, LOW);
digitalWrite(RewRightB, LOW);
// Plus extra click to be sure they close
} else { // One at a time for "normal rewards test"
digitalWrite(RewLeftA, HIGH);
delay(rate);
digitalWrite(RewLeftA, LOW);
delay(rate / 2);
digitalWrite(RewLeftB, HIGH);
delay(rate);
digitalWrite(RewLeftB, LOW);
delay(rate / 2);
digitalWrite(RewRightA, HIGH);
delay(rate);
digitalWrite(RewRightA, LOW);
delay(rate / 2);
digitalWrite(RewRightB, HIGH);
delay(rate);
digitalWrite(RewRightB, LOW);
}
} // End Rew V Test
void GiveReward(byte RewLoc, int Drops) { // RewPin, # of drops
// Coded with delays so this core will be here until the reward period is complete
// New in v2.8 clear licks from count that were during reward
// Report which reward and which side with A B C D
// Tell to ML for Trial List Results Using L and R for licks
unsigned long RwTm = millis(); // Rew Takes time, so mark time now
int DropTm; // v45 separate for each valve
// Uses A B C D (ML sends a b c d for manual)
// RewLoc is pin #
switch (RewLoc) {
case RewLeftA:
DropTm = DropTmA;
break;
case RewLeftB:
DropTm = DropTmB;
break;
case RewRightA:
DropTm = DropTmC;
break;
case RewRightB:
DropTm = DropTmD;
break;
}
// give 1 drop for Rew Every v48
if (RewEvery)
Drops = 1;
// Need to use microseconds for better accuracy than single ms
for (int drp = 0; drp < Drops; drp++) {
digitalWrite(RewLoc, HIGH);
delayMicroseconds(DropTm); // DropTm now in uS v43, separate for each valve
digitalWrite(RewLoc, LOW);
delay(DropDelay - int(DropTm / 1000)); // for loop total time is DropDelay
}
if ((DropDelay > MinDD) && (DropDelay < 900) && (RewEvery == 0) && ManRew == false) {
// Either a cleaning if > 0.9 sec so no wait, or training (==50)
// also no extra waiting for RewEvery or Manual Rewards
for (int i = Drops; i < MaxDrops; i++) {
delay(DropDelay);
}
}
ManRew = false; // Set it back
// v2.8 Reset lick counts to avoid rewarding extra rewards for licking rewards
LickCountL = 0; // Reset licks
LickCountR = 0; // Reset licks
// Tell which reward was given
switch (RewLoc) { //
case RewLeftA:
Serial.printf("A%u\r\n", RwTm);
break;
case RewLeftB:
Serial.printf("B%u\r\n", RwTm);
break;
case RewRightA:
Serial.printf("C%u\r\n", RwTm);
break;
case RewRightB:
Serial.printf("D%u\r\n", RwTm);
break;
} // end switch
} // end Give Reward
void ErrorBuzz(int ErrNum) { // actually only 1 kind of error in this program
tone(BUZZER_PIN, Buzz, BuzzTm); //
// No reporting of Error number currently
// for Buzz test can use 0 to get no report
if (ErrNum > 0) { // Buzz without E code, for z
Serial.printf("E%u\r\n", millis()); // T Logging
#ifdef DEBUG // info to debugger
// Atomic println
switch (ErrNum) {
case 1:
Serial.printf("#Err L Air %u\r\n ", millis());
break;
case 2: