forked from jnsgruk/nixos-config
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Working SamcoEnhanced.ino
4060 lines (3730 loc) · 177 KB
/
Working SamcoEnhanced.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
/*!
* @file SamcoEnhanced.ino
* @brief IR-GUN4ALL - 4IR LED Lightgun sketch w/ support for force feedback and other features.
* Based on Prow's Enhanced Fork from https://github.com/Prow7/ir-light-gun,
* Which in itself is based on the 4IR Beta "Big Code Update" SAMCO project from https://github.com/samuelballantyne/IR-Light-Gun
*
* @copyright Samco, https://github.com/samuelballantyne, June 2020
* @copyright Mike Lynch, July 2021
* @copyright That One Seong, https://github.com/SeongGino, October 2023
* @copyright GNU Lesser General Public License
*
* @author [Sam Ballantyne](samuelballantyne@hotmail.com)
* @author Mike Lynch
* @author [That One Seong](SeongsSeongs@gmail.com)
* @date 2023
*/
#define G4ALL_VERSION 2.1 // 2.1.1 Borrowed code for LED to work on Arduino Nano RP2040 Connect from the glorious Seong ... I probably broke normal LED support lol
#define G4ALL_CODENAME "Fantasyland-rev1"
// Remember to check out the enclosed instruction book (README.md) that came with this program for more information!
#include <Arduino.h>
#include <SamcoBoard.h>
// include TinyUSB or HID depending on USB stack option
#if defined(USE_TINYUSB)
#include <Adafruit_TinyUSB.h>
#elif defined(CFG_TUSB_MCU)
#error Incompatible USB stack. Use Adafruit TinyUSB.
#else
// Arduino USB stack (currently not supported, will not build)
#include <HID.h>
#endif
#include <TinyUSB_Devices.h>
#include <Wire.h>
#ifdef DOTSTAR_ENABLE
#define LED_ENABLE
#include <Adafruit_DotStar.h>
#endif // DOTSTAR_ENABLE
#ifdef NEOPIXEL_PIN
#define LED_ENABLE
#include <Adafruit_NeoPixel.h>
#endif
#ifdef SAMCO_FLASH_ENABLE
#include <Adafruit_SPIFlashBase.h>
#elif SAMCO_EEPROM_ENABLE
#include <EEPROM.h>
#endif // SAMCO_FLASH_ENABLE/EEPROM_ENABLE
#include <DFRobotIRPositionEx.h>
#include <WiFiNINA.h>
#include <LightgunButtons.h>
#include <SamcoPositionEnhanced.h>
#include <SamcoConst.h>
#include "SamcoColours.h"
#include "SamcoPreferences.h"
#ifdef ARDUINO_ARCH_RP2040
#include <hardware/pwm.h>
#include <hardware/irq.h>
// declare PWM ISR
void rp2040pwmIrq(void);
#endif
// IMPORTANT ADDITIONS HERE ------------------------------------------------------------------------------- (*****THE HARDWARE SETTINGS YOU WANNA TWEAK!*****)
// Enables input processing on the second core, if available. Currently exclusive to Raspberry Pi Pico, or boards based on the RP2040.
// Isn't necessarily faster, but might make responding to force feedback more consistent.
// If unsure, leave this uncommented - it only affects RP2040 anyways.
#define DUAL_CORE
// Here we define the Manufacturer Name/Device Name/PID:VID of the gun as will be displayed by the operating system.
// For multiplayer, different guns need different IDs!
// If unsure, or are only using one gun, just leave these at their defaults!
#define MANUFACTURER_NAME "IgguT"
#define DEVICE_NAME "LIGHTGUN"
#define DEVICE_VID 0x0920
#define DEVICE_PID 0x1998
// Set what player this board is mapped to by default (1-4). This will change keyboard mappings appropriate for the respective player.
// If unsure, just leave this at 1 - the mapping can be changed at runtime by sending an 'XR#' command over Serial, where # = player number
#define PLAYER_NUMBER 1
// Leave this uncommented to enable MAMEHOOKER support, or comment out (//) to disable references to serial reading and only use it for debugging.
// WARNING: Has a chance of making the board lock up if TinyUSB hasn't been patched to fix serial-related lockups.
// If you're building this for RP2040, please make sure that you have NOT installed the TinyUSB library.
// If unsure, leave uncommented - serial activity is used for configuration, and undefining this will cause errors.
#define MAMEHOOKER
// Leave this uncommented if your build uses hardware switches, or comment out to disable all references to hw switch functionality.
//#define USES_SWITCHES
#ifdef USES_SWITCHES // Here's where they should be defined!
const byte autofireSwitch = -1; // What's the pin number of the autofire switch? Digital.
#endif // USES_SWITCHES
// Leave this uncommented if your build uses a rumble motor; comment out to disable any references to rumble functionality.
#define USES_RUMBLE
#ifdef USES_RUMBLE
bool rumbleActive = true; // Are we allowed to do rumble? Default to off.
#ifdef USES_SWITCHES
const byte rumbleSwitch = -1; // What's the pin number of the rumble switch? Digital.
#endif // USES_SWITCHES
// If you'd rather not use a solenoid for force-feedback effects, this will change all on-screen force feedback events to use the motor instead.
// TODO: actually finish this.
//#define RUMBLE_FF
#if defined(RUMBLE_FF) && defined(USES_SOLENOID)
#error Rumble Force-feedback is incompatible with Solenoids! Use either one or the other.
#endif // RUMBLE_FF && USES_SOLENOID
#endif // USES_RUMBLE
// Leave this uncommented if your build uses a solenoid, or comment out to disable any references to solenoid functionality.
#define USES_SOLENOID
#ifdef USES_SOLENOID
bool solenoidActive = true; // Are we allowed to use a solenoid? Default to off.
#ifdef USES_SWITCHES // If your build uses hardware switches,
const byte solenoidSwitch = -1; // What's the pin number of the solenoid switch? Digital.
#endif // USES_SWITCHES
// Uncomment if your build uses a TMP36 temperature sensor for a solenoid, or comment out if your solenoid doesn't need babysitting.
//#define USES_TEMP
#ifdef USES_TEMP
const byte tempPin = A0; // What's the pin number of the temp sensor? Needs to be analog.
const byte tempNormal = 50; // Solenoid: Anything below this value is "normal" operating temperature for the solenoid, in Celsius.
const byte tempWarning = 60; // Solenoid: Above normal temps, this is the value up to where we throttle solenoid activation, in Celsius.
#endif // USES_TEMP // **Anything above ^this^ is considered too dangerous, will disallow any further engagement.
#endif // USES_SOLENOID
// Which software extras should be activated? Set here if your build doesn't use toggle switches.
bool autofireActive = false; // Is solenoid firing in autofire (rapid) mode? false = default single shot, true = autofire
bool offscreenButton = true; // Does shooting offscreen also send a button input (for buggy games that don't recognize off-screen shots)? Default to off.
bool burstFireActive = false; // Is the solenoid firing in burst fire mode? false = default, true = 3-shot burst firing mode
// Pins setup - where do things be plugged into like? Uses GPIO codes ONLY! See also: https://learn.adafruit.com/adafruit-itsybitsy-rp2040/pinouts
int8_t rumblePin = 17; // What's the pin number of the rumble output? Needs to be digital.
int8_t solenoidPin = 16; // What's the pin number of the solenoid output? Needs to be digital.
int8_t btnTrigger = 15; // Programmer's note: made this just to simplify the trigger pull detection, guh.
int8_t btnGunA = 0; // <-- GCon 1-spec
int8_t btnGunB = 1; // <-- GCon 1-spec
int8_t btnGunC = 18; // Everything below are for GCon 2-spec only
int8_t btnStart = 19;
int8_t btnSelect = 20;
int8_t btnGunUp = -1;
int8_t btnGunDown = -1;
int8_t btnGunLeft = -1;
int8_t btnGunRight = -1;
int8_t btnPedal = -1;
int8_t btnPump = -1;
int8_t btnHome = -1;
// If your build uses an analog stick, unset and define the stick's pins here!
// Remember: ANALOG PINS ONLY!
//#define USES_ANALOG
#ifdef USES_ANALOG
int8_t analogPinX = -1;
int8_t analogPinY = -1;
#endif // USES_ANALOG
// If you're using a regular 4-pin RGB LED, unset and define the R/G/B color pins here!
// Remember: PWM PINS ONLY!
#define FOURPIN_LED
#ifdef FOURPIN_LED
#define LED_ENABLE
int8_t PinR = 27;
int8_t PinG = 25;
int8_t PinB = 26;
// Set if your LED is Common Anode (+, connects to 5V) rather than Common Cathode (-, connects to GND)
bool commonAnode = true;
#endif // FOURPIN_LED
// If you're using a custom external NeoPixel unit, unset and define the data pin location here!
// Any digital pin is fine for NeoPixels. Currently we only use the first "pixel".
//#define CUSTOM_NEOPIXEL
#ifdef CUSTOM_NEOPIXEL
#define LED_ENABLE
#include <Adafruit_NeoPixel.h>
int8_t customLEDpin = -1; // Pin number for the custom NeoPixel (strip) being used.
uint8_t customLEDcount = 1; // Amount of pixels; if not using a strip, just set to 1.
#endif // CUSTOM_NEOPIXEL
// Adjustable aspects:
byte autofireWaitFactor = 3; // This is the default time to wait between rapid fire pulses (from 2-4)
#ifdef USES_RUMBLE
const byte rumbleIntensity = 255; // The strength of the rumble motor, 0=off to 255=maxPower.
const unsigned int rumbleInterval = 110; // How long to wait for the whole rumble command, in ms.
#endif // USES_RUMBLE
#ifdef USES_SOLENOID
const unsigned int solenoidNormalInterval = 45; // Interval for solenoid activation, in ms.
const unsigned int solenoidFastInterval = 30; // Interval for faster solenoid activation, in ms.
const unsigned int solenoidLongInterval = 500; // for single shot, how long to wait until we start spamming the solenoid? In ms.
#endif // USES_SOLENOID
// Menu options:
bool simpleMenu = true; // Flag that determines if Pause Mode will be a simple scrolling menu; else, relies on hotkeys.
bool holdToPause = true; // Flag that determines if Pause Mode is invoked by a button combo hold (EnterPauseModeHoldBtnMask) when pointing offscreen; else, relies on EnterPauseModeBtnMask hotkey.
unsigned int pauseHoldLength = 4000; // How long the combo should be held for, in ms.
//--------------------------------------------------------------------------------------------------------------------------------------
// Sanity checks and assignments for player number -> common keyboard assignments
#if PLAYER_NUMBER == 1
char playerStartBtn = '1';
char playerSelectBtn = '5';
#elif PLAYER_NUMBER == 2
char playerStartBtn = '2';
char playerSelectBtn = '6';
#elif PLAYER_NUMBER == 3
char playerStartBtn = '3';
char playerSelectBtn = '7';
#elif PLAYER_NUMBER == 4
char playerStartBtn = '4';
char playerSelectBtn = '8';
#else
#error Undefined or out-of-range player number! Please set PLAYER_NUMBER to 1, 2, 3, or 4.
#endif // PLAYER_NUMBER
// enable extra serial debug during run mode
//#define PRINT_VERBOSE 1
//#define DEBUG_SERIAL 1
//#define DEBUG_SERIAL 2
// extra position glitch filtering,
// not required after discoverving the DFRobotIRPositionEx atomic read technique
//#define EXTRA_POS_GLITCH_FILTER
// numbered index of buttons, must match ButtonDesc[] order
enum ButtonIndex_e {
BtnIdx_Trigger = 0,
BtnIdx_A,
BtnIdx_B,
BtnIdx_Start,
BtnIdx_Select,
BtnIdx_Up,
BtnIdx_Down,
BtnIdx_Left,
BtnIdx_Right,
BtnIdx_Reload,
BtnIdx_Pedal,
BtnIdx_Pump,
BtnIdx_Home
};
// bit mask for each button, must match ButtonDesc[] order to match the proper button events
enum ButtonMask_e {
BtnMask_Trigger = 1 << BtnIdx_Trigger,
BtnMask_A = 1 << BtnIdx_A,
BtnMask_B = 1 << BtnIdx_B,
BtnMask_Start = 1 << BtnIdx_Start,
BtnMask_Select = 1 << BtnIdx_Select,
BtnMask_Up = 1 << BtnIdx_Up,
BtnMask_Down = 1 << BtnIdx_Down,
BtnMask_Left = 1 << BtnIdx_Left,
BtnMask_Right = 1 << BtnIdx_Right,
BtnMask_Reload = 1 << BtnIdx_Reload,
BtnMask_Pedal = 1 << BtnIdx_Pedal,
BtnMask_Pump = 1 << BtnIdx_Pump,
BtnMask_Home = 1 << BtnIdx_Home
};
// Button descriptor
// The order of the buttons is the order of the button bitmask
// must match ButtonIndex_e order, and the named bitmask values for each button
// see LightgunButtons::Desc_t, format is:
// {pin, report type, report code (ignored for internal), offscreen report type, offscreen report code, gamepad output report type, gamepad output report code, debounce time, debounce mask, label}
LightgunButtons::Desc_t LightgunButtons::ButtonDesc[] = {
{btnTrigger, LightgunButtons::ReportType_Internal, MOUSE_LEFT, LightgunButtons::ReportType_Internal, MOUSE_LEFT, LightgunButtons::ReportType_Internal, 0, 15, BTN_AG_MASK}, // Barry says: "I'll handle this."
{btnGunA, LightgunButtons::ReportType_Mouse, MOUSE_RIGHT, LightgunButtons::ReportType_Mouse, MOUSE_RIGHT, LightgunButtons::ReportType_Gamepad, 1, 15, BTN_AG_MASK2},
{btnGunB, LightgunButtons::ReportType_Mouse, MOUSE_MIDDLE, LightgunButtons::ReportType_Mouse, MOUSE_MIDDLE, LightgunButtons::ReportType_Gamepad, 2, 15, BTN_AG_MASK2},
{btnStart, LightgunButtons::ReportType_Internal, playerStartBtn, LightgunButtons::ReportType_Internal, playerStartBtn, LightgunButtons::ReportType_Gamepad, 5, 20, BTN_AG_MASK2},
{btnSelect, LightgunButtons::ReportType_Internal, playerSelectBtn, LightgunButtons::ReportType_Internal, playerSelectBtn, LightgunButtons::ReportType_Gamepad, 6, 20, BTN_AG_MASK2},
{btnGunUp, LightgunButtons::ReportType_Keyboard, KEY_UP_ARROW, LightgunButtons::ReportType_Keyboard, KEY_UP_ARROW, LightgunButtons::ReportType_Gamepad, 7, 20, BTN_AG_MASK2},
{btnGunDown, LightgunButtons::ReportType_Keyboard, KEY_DOWN_ARROW, LightgunButtons::ReportType_Keyboard, KEY_DOWN_ARROW, LightgunButtons::ReportType_Gamepad, 8, 20, BTN_AG_MASK2},
{btnGunLeft, LightgunButtons::ReportType_Keyboard, KEY_LEFT_ARROW, LightgunButtons::ReportType_Keyboard, KEY_LEFT_ARROW, LightgunButtons::ReportType_Gamepad, 9, 20, BTN_AG_MASK2},
{btnGunRight, LightgunButtons::ReportType_Keyboard, KEY_RIGHT_ARROW, LightgunButtons::ReportType_Keyboard, KEY_RIGHT_ARROW, LightgunButtons::ReportType_Gamepad, 10, 20, BTN_AG_MASK2},
{btnGunC, LightgunButtons::ReportType_Mouse, MOUSE_BUTTON4, LightgunButtons::ReportType_Mouse, MOUSE_BUTTON4, LightgunButtons::ReportType_Gamepad, 3, 15, BTN_AG_MASK2},
{btnPedal, LightgunButtons::ReportType_Mouse, MOUSE_BUTTON5, LightgunButtons::ReportType_Mouse, MOUSE_BUTTON5, LightgunButtons::ReportType_Gamepad, 4, 15, BTN_AG_MASK2},
{btnPump, LightgunButtons::ReportType_Mouse, MOUSE_RIGHT, LightgunButtons::ReportType_Mouse, MOUSE_RIGHT, LightgunButtons::ReportType_Gamepad, 1, 15, BTN_AG_MASK2},
{btnHome, LightgunButtons::ReportType_Internal, MOUSE_BUTTON5, LightgunButtons::ReportType_Internal, MOUSE_BUTTON5, LightgunButtons::ReportType_Internal, 0, 15, BTN_AG_MASK2}
};
// button count constant
constexpr unsigned int ButtonCount = sizeof(LightgunButtons::ButtonDesc) / sizeof(LightgunButtons::ButtonDesc[0]);
// button runtime data arrays
LightgunButtonsStatic<ButtonCount> lgbData;
// button object instance
LightgunButtons buttons(lgbData, ButtonCount);
/*
// WIP, some sort of generic button handler table for pause mode
// pause button function
typedef void (*PauseModeBtnFn_t)();
// pause mode function
typedef struct PauseModeFnEntry_s {
uint32_t buttonMask;
PauseModeBtnFn_t pfn;
} PauseModeFnEntry_t;
*/
// button combo to send an escape keypress
uint32_t EscapeKeyBtnMask = BtnMask_Reload | BtnMask_Start;
// button combo to enter pause mode
uint32_t EnterPauseModeBtnMask = BtnMask_Reload | BtnMask_Select;
// button combo to enter pause mode (holding ver)
uint32_t EnterPauseModeHoldBtnMask = BtnMask_Trigger | BtnMask_A;
// press any button to enter pause mode from Processing mode (this is not a button combo)
uint32_t EnterPauseModeProcessingBtnMask = BtnMask_A | BtnMask_B | BtnMask_Reload;
// button combo to exit pause mode back to run mode
uint32_t ExitPauseModeBtnMask = BtnMask_Reload;
// press and hold any button to exit simple pause menu (this is not a button combo)
uint32_t ExitPauseModeHoldBtnMask = BtnMask_A | BtnMask_B;
// press any button to cancel the calibration (this is not a button combo)
uint32_t CancelCalBtnMask = BtnMask_Reload | BtnMask_Start | BtnMask_Select;
// button combo to skip the center calibration step
uint32_t SkipCalCenterBtnMask = BtnMask_A;
// button combo to save preferences to non-volatile memory
uint32_t SaveBtnMask = BtnMask_Start | BtnMask_Select;
// button combo to increase IR sensitivity
uint32_t IRSensitivityUpBtnMask = BtnMask_B | BtnMask_Select;
// button combo to decrease IR sensitivity
uint32_t IRSensitivityDownBtnMask = BtnMask_A | BtnMask_B | BtnMask_Select;
// button combinations to select a run mode
uint32_t RunModeNormalBtnMask = BtnMask_Start | BtnMask_A;
uint32_t RunModeAverageBtnMask = BtnMask_Start | BtnMask_B;
uint32_t RunModeProcessingBtnMask = BtnMask_Start | BtnMask_Right;
// button combination to toggle offscreen button mode in software:
uint32_t OffscreenButtonToggleBtnMask = BtnMask_Reload | BtnMask_A;
// button combination to toggle offscreen button mode in software:
uint32_t AutofireSpeedToggleBtnMask = BtnMask_Reload | BtnMask_B;
#ifndef USES_SWITCHES
// button combination to toggle rumble in software:
uint32_t RumbleToggleBtnMask = BtnMask_Select;
// button combination to toggle solenoid in software:
uint32_t SolenoidToggleBtnMask = BtnMask_Start;
#endif
// colour when no IR points are seen
uint32_t IRSeen0Color = WikiColor::Amber;
// colour when calibrating
uint32_t CalModeColor = WikiColor::Red;
// number of profiles
constexpr byte ProfileCount = 4;
// run modes
// note that this is a 5 bit value when stored in the profiles
enum RunMode_e {
RunMode_Normal = 0, ///< Normal gun mode, no averaging
RunMode_Average = 1, ///< 2 frame moving average
RunMode_Average2 = 2, ///< weighted average with 3 frames
RunMode_ProfileMax = 2, ///< maximum mode allowed for profiles
RunMode_Processing = 3, ///< Processing test mode
RunMode_Count
};
// profiles ----------------------------------------------------------------------------------------------
// defaults can be populated here, but any values in EEPROM/Flash will override these.
// if you have original Samco calibration values, multiply by 4 for the center position and
// scale is multiplied by 1000 and stored as an unsigned integer, see SamcoPreferences::Calibration_t
SamcoPreferences::ProfileData_t profileData[ProfileCount] = {
{0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Average, 0, 0},
{0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Average, 0, 0},
{0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Average, 0, 0},
{0, 0, 0, 0, DFRobotIRPositionEx::Sensitivity_Default, RunMode_Average, 0, 0}
};
// ------------------------------------------------------------------------------------------------------
// profile descriptor
typedef struct ProfileDesc_s {
// button(s) to select the profile
uint32_t buttonMask;
// LED colour
uint32_t color;
// button label
const char* buttonLabel;
// optional profile label
const char* profileLabel;
} ProfileDesc_t;
// profile descriptor
static const ProfileDesc_t profileDesc[ProfileCount] = {
{BtnMask_A, WikiColor::Cerulean_blue, "A", "TV Fisheye Lens"},
{BtnMask_B, WikiColor::Cornflower_blue, "B", "TV Wide-angle Lens"},
{BtnMask_Start, WikiColor::Green, "Start", "TV"},
{BtnMask_Select, WikiColor::Green_Lizard, "Select", "Monitor"}
};
// overall calibration defaults, no need to change if data saved to NV memory or populate the profile table
// see profileData[] array below for specific profile defaults
int xCenter = MouseMaxX / 2;
int yCenter = MouseMaxY / 2;
float xScale = 1.64;
float yScale = 0.95;
// step size for adjusting the scale
constexpr float ScaleStep = 0.001;
int finalX = 0; // Values after tilt correction
int finalY = 0;
int moveXAxis = 0; // Unconstrained mouse postion
int moveYAxis = 0;
int moveXAxisArr[3] = {0, 0, 0};
int moveYAxisArr[3] = {0, 0, 0};
int moveIndex = 0;
int conMoveXAxis = 0; // Constrained mouse postion
int conMoveYAxis = 0;
// ADDITIONS HERE: the boring inits related to the things I added.
// Offscreen bits:
//bool offScreen = false; // To tell if we're off-screen, if offXAxis & offYAxis are true. Part of LightgunButtons now.
bool offXAxis = false; // Supercedes the inliner every trigger pull, we just check each axis individually.
bool offYAxis = false;
// Boring values for the solenoid timing stuff:
#ifdef USES_SOLENOID
unsigned long previousMillisSol = 0; // our timer (holds the last time since a successful interval pass)
bool solenoidFirstShot = false; // default to off, but actually set this the first time we shoot.
#ifdef USES_TEMP
int tempSensor; // Temp sensor changes over time, so just initialize the variable here ig.
const unsigned int solenoidWarningInterval = solenoidFastInterval * 3; // for if solenoid is getting toasty.
#endif // USES_TEMP
#endif // USES_SOLENOID
// For burst firing stuff:
byte burstFireCount = 0; // What shot are we on?
byte burstFireCountLast = 0; // What shot have we last processed?
bool burstFiring = false; // Are we in a burst fire command?
// For offscreen button stuff:
bool offscreenBShot = false; // For offscreenButton functionality, to track if we shot off the screen.
bool buttonPressed = false; // Sanity check.
// For autofire:
bool triggerHeld = false; // Trigger SHOULDN'T be being pulled by default, right?
// For rumble:
#ifdef USES_RUMBLE
unsigned long previousMillisRumble = 0; // our time since the rumble motor event started
bool rumbleHappening = false; // To keep track on if this is a rumble command or not.
bool rumbleHappened = false; // If we're holding, this marks we sent a rumble command already.
// We need the rumbleHappening because of the variable nature of the PWM controlling the motor.
#endif // USES_RUMBLE
#ifdef USES_ANALOG
bool analogIsValid; // Flag set true if analog stick is mapped to valid nums
bool analogStickPolled = false; // Flag set on if the stick was polled recently, to prevent overloading with aStick updates.
#endif // USES_ANALOG
#ifdef FOURPIN_LED
bool ledIsValid; // Flag set true if RGB pins are mapped to valid numbers
#endif // FOURPIN_LED
// For button queuing:
byte buttonsHeld = 0b00000000; // Bitmask of what aux buttons we've held on (for btnStart through btnRight)
#ifdef MAMEHOOKER
// For serial mode:
bool serialMode = false; // Set if we're prioritizing force feedback over serial commands or not.
bool offscreenButtonSerial = false; // Serial-only version of offscreenButton toggle.
byte serialQueue = 0b00000000; // Bitmask of events we've queued from the serial receipt.
// from least to most significant bit: solenoid digital, solenoid pulse, rumble digital, rumble pulse, R/G/B direct, RGB (any) pulse.
#ifdef LED_ENABLE
unsigned long serialLEDPulsesLastUpdate = 0; // The timestamp of the last serial-invoked LED pulse update we iterated.
unsigned int serialLEDPulsesLength = 2; // How long each stage of a serial-invoked pulse rumble is, in ms.
bool serialLEDChange = false; // Set on if we set an LED command this cycle.
bool serialLEDPulseRising = true; // In LED pulse events, is it rising now? True to indicate rising, false to indicate falling; default to on for very first pulse.
byte serialLEDPulses = 0; // How many LED pulses are we being told to do?
byte serialLEDPulsesLast = 0; // What LED pulse we've processed last.
byte serialLEDR = 0; // For the LED, how strong should it be?
byte serialLEDG = 0; // Each channel is defined as three brightness values
byte serialLEDB = 0; // So yeah.
byte serialLEDPulseColorMap = 0b00000000; // The map of what LEDs should be pulsing (we use the rightmost three of this bitmask for R, G, or B).
#endif // LED_ENABLE
#ifdef USES_RUMBLE
unsigned long serialRumbPulsesLastUpdate = 0; // The timestamp of the last serial-invoked pulse rumble we updated.
unsigned int serialRumbPulsesLength = 60; // How long each stage of a serial-invoked pulse rumble is, in ms.
byte serialRumbPulseStage = 0; // 0 = start/rising, 1 = peak, 2 = falling, 3 = reset to start
byte serialRumbPulses = 0; // If rumble is commanded to do pulse responses, how many?
byte serialRumbPulsesLast = 0; // Counter of how many pulse rumbles we did so far.
#endif // USES_RUMBLE
#ifdef USES_SOLENOID
unsigned long serialSolPulsesLastUpdate = 0; // The timestamp of the last serial-invoked pulse solenoid event we updated.
unsigned int serialSolPulsesLength = 80; // How long to wait between each solenoid event in a pulse, in ms.
bool serialSolPulseOn = false; // Because we can't just read it normally, is the solenoid getting PWM high output now?
int serialSolPulses = 0; // How many solenoid pulses are we being told to do?
int serialSolPulsesLast = 0; // What solenoid pulse we've processed last.
#endif // USES_SOLENOID
#endif // MAMEHOOKER
unsigned int lastSeen = 0;
bool justBooted = true; // For ops we need to do on initial boot (just joystick centering atm)
#ifdef EXTRA_POS_GLITCH_FILTER00
int badFinalTick = 0;
int badMoveTick = 0;
int badFinalCount = 0;
int badMoveCount = 0;
// number of consecutive bad move values to filter
constexpr unsigned int BadMoveCountThreshold = 3;
// Used to filter out large jumps/glitches
constexpr int BadMoveThreshold = 49 * CamToMouseMult;
#endif // EXTRA_POS_GLITCH_FILTER
// profile in use
unsigned int selectedProfile = 0;
// IR positioning camera
#ifdef ARDUINO_ADAFRUIT_ITSYBITSY_RP2040
// ItsyBitsy RP2040 only exposes I2C1 pins on the board
DFRobotIRPositionEx dfrIRPos(Wire1);
#else // Pico et al does not have this limitation
//DFRobotIRPosition myDFRobotIRPosition;
DFRobotIRPositionEx dfrIRPos(Wire);
#endif
// Samco positioning
SamcoPositionEnhanced mySamco;
// operating modes
enum GunMode_e {
GunMode_Init = -1,
GunMode_Run = 0,
GunMode_CalHoriz = 1,
GunMode_CalVert = 2,
GunMode_CalCenter = 3,
GunMode_Pause = 4
};
GunMode_e gunMode = GunMode_Init; // initial mode
enum PauseModeSelection_e {
PauseMode_Calibrate = 0,
PauseMode_ProfileSelect,
PauseMode_Save,
#ifndef USES_SWITCHES
#ifdef USES_RUMBLE
PauseMode_RumbleToggle,
#endif // USES_RUMBLE
#endif // USES_SWITCHES
#ifdef USES_SOLENOID
#ifndef USES_SWITCHES
PauseMode_SolenoidToggle,
#endif // USES_SWITCHES
//PauseMode_BurstFireToggle,
#endif // USES_SOLENOID
PauseMode_EscapeSignal
};
// Selector for which option in the simple pause menu you're scrolled on.
byte pauseModeSelection = 0;
// Selector for which profile in the profile selector of the simple pause menu you're picking.
byte profileModeSelection;
// Flag to tell if we're in the profile selector submenu of the simple pause menu.
bool pauseModeSelectingProfile = false;
// Timestamp of when we started holding a buttons combo.
unsigned long pauseHoldStartstamp;
bool pauseHoldStarted = false;
bool pauseExitHoldStarted = false;
// run mode
RunMode_e runMode = RunMode_Normal;
// IR camera sensitivity
DFRobotIRPositionEx::Sensitivity_e irSensitivity = DFRobotIRPositionEx::Sensitivity_Default;
static const char* RunModeLabels[RunMode_Count] = {
"Normal",
"Averaging",
"Averaging2",
"Processing"
};
// preferences saved in non-volatile memory, populated with defaults
SamcoPreferences::Preferences_t SamcoPreferences::preferences = {
profileData, ProfileCount, // profiles
0, // default profile
};
enum StateFlag_e {
// print selected profile once per pause state when the COM port is open
StateFlag_PrintSelectedProfile = (1 << 0),
// report preferences once per pause state when the COM port is open
StateFlag_PrintPreferences = (1 << 1),
// enable save (allow save once per pause state)
StateFlag_SavePreferencesEn = (1 << 2),
// print preferences storage
StateFlag_PrintPreferencesStorage = (1 << 3)
};
// when serial connection resets, these flags are set
constexpr uint32_t StateFlagsDtrReset = StateFlag_PrintSelectedProfile | StateFlag_PrintPreferences | StateFlag_PrintPreferencesStorage;
// state flags, see StateFlag_e
uint32_t stateFlags = StateFlagsDtrReset;
// internal addressable LEDs inits
#ifdef DOTSTAR_ENABLE
// note if the colours don't match then change the colour format from BGR
// apparently different lots of DotStars may have different colour ordering ¯\_(ツ)_/¯
Adafruit_DotStar dotstar(1, DOTSTAR_DATAPIN, DOTSTAR_CLOCKPIN, DOTSTAR_BGR);
#endif // DOTSTAR_ENABLE
#if defined(NEOPIXEL_PIN) && defined(CUSTOM_NEOPIXEL)
Adafruit_NeoPixel neopixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel externPixel(customLEDcount, customLEDpin, NEO_GRB + NEO_KHZ800);
#elif defined(CUSTOM_NEOPIXEL)
Adafruit_NeoPixel externPixel(customLEDcount, customLEDpin, NEO_GRB + NEO_KHZ800);
#elif defined(NEOPIXEL_PIN)
Adafruit_NeoPixel neopixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
#endif // CUSTOM_NEOPIXEL
// flash transport instance
#if defined(EXTERNAL_FLASH_USE_QSPI)
Adafruit_FlashTransport_QSPI flashTransport;
#elif defined(EXTERNAL_FLASH_USE_SPI)
Adafruit_FlashTransport_SPI flashTransport(EXTERNAL_FLASH_USE_CS, EXTERNAL_FLASH_USE_SPI);
#endif
#ifdef SAMCO_FLASH_ENABLE
// Adafruit_SPIFlashBase non-volatile storage
// flash instance
Adafruit_SPIFlashBase flash(&flashTransport);
static const char* NVRAMlabel = "Flash";
// flag to indicate if non-volatile storage is available
// this will enable in setup()
bool nvAvailable = false;
#endif // SAMCO_FLASH_ENABLE
#ifdef SAMCO_EEPROM_ENABLE
// EEPROM non-volatile storage
static const char* NVRAMlabel = "EEPROM";
// flag to indicate if non-volatile storage is available
// unconditional for EEPROM
bool nvAvailable = true;
#endif
// non-volatile preferences error code
int nvPrefsError = SamcoPreferences::Error_NoStorage;
// preferences instance
SamcoPreferences samcoPreferences;
// number of times the IR camera will update per second
constexpr unsigned int IRCamUpdateRate = 209;
#ifdef SAMCO_NO_HW_TIMER
// use the millis() or micros() counter instead
unsigned long irPosUpdateTime = 0;
// will set this to 1 when the IR position can update
unsigned int irPosUpdateTick = 0;
#define SAMCO_NO_HW_TIMER_UPDATE() NoHardwareTimerCamTickMillis()
//define SAMCO_NO_HW_TIMER_UPDATE() NoHardwareTimerCamTickMicros()
#else
// timer will set this to 1 when the IR position can update
volatile unsigned int irPosUpdateTick = 0;
#endif // SAMCO_NO_HW_TIMER
#ifdef DEBUG_SERIAL
static unsigned long serialDbMs = 0;
static unsigned long frameCount = 0;
static unsigned long irPosCount = 0;
#endif
// used for periodic serial prints
unsigned long lastPrintMillis = 0;
#ifdef USE_TINYUSB
// AbsMouse5 instance (this is hardcoded as the second definition, so weh)
AbsMouse5_ AbsMouse5(2);
#else
// AbsMouse5 instance
AbsMouse5_ AbsMouse5(1);
#endif
void setup() {
Serial.begin(9600); // 9600 = 1ms data transfer rates, default for MAMEHOOKER COM devices.
Serial.setTimeout(0);
#ifdef SAMCO_FLASH_ENABLE
// init flash and load saved preferences
nvAvailable = flash.begin();
#else
#if defined(SAMCO_EEPROM_ENABLE) && defined(ARDUINO_ARCH_RP2040)
// initialize EEPROM device. Arduino AVR has a 1k flash, so use that.
EEPROM.begin(1024);
#endif // SAMCO_EEPROM_ENABLE/RP2040
#endif // SAMCO_FLASH_ENABLE
if(nvAvailable) {
LoadPreferences();
}
// use values from preferences
ApplyInitialPrefs();
// We're setting our custom USB identifiers, as defined in the configuration area!
#ifdef USE_TINYUSB
TinyUSBDevice.setManufacturerDescriptor(MANUFACTURER_NAME);
TinyUSBDevice.setProductDescriptor(DEVICE_NAME);
TinyUSBDevice.setID(DEVICE_VID, DEVICE_PID);
#endif // USE_TINYUSB
#ifdef USES_RUMBLE
pinMode(rumblePin, OUTPUT);
#endif // USES_RUMBLE
#ifdef USES_SOLENOID
pinMode(solenoidPin, OUTPUT);
#endif // USES_SOLENOID
#ifdef USES_SWITCHES
#ifdef USES_RUMBLE
pinMode(rumbleSwitch, INPUT_PULLUP);
#endif // USES_RUMBLE
#ifdef USES_SOLENOID
pinMode(solenoidSwitch, INPUT_PULLUP);
#endif // USES_SOLENOID
pinMode(autofireSwitch, INPUT_PULLUP);
#endif // USES_SWITCHES
#ifdef LED_ENABLE
LedInit();
#endif // LED_ENABLE
#ifdef USES_ANALOG
analogReadResolution(12);
#ifdef USES_TEMP
if(analogPinX >= 0 && analogPinY >= 0 && analogPinX != analogPinY &&
analogPinX != tempPin && analogPinY != tempPin) {
#else
if(analogPinX >= 0 && analogPinY >= 0 && analogPinX != analogPinY) {
#endif
//pinMode(analogPinX, INPUT);
//pinMode(analogPinY, INPUT);
analogIsValid = true;
} else {
analogIsValid = false;
}
#endif // USES_ANALOG
// Wire (0) should be defaulting to and using GPIO 4 & 5, but in case you need it:
Wire.setSDA(12);
Wire.setSCL(13);
#if !defined(ARDUINO_ARCH_RP2040) || !defined(DUAL_CORE)
// initialize buttons (on the main thread for single core systems)
buttons.Begin();
#endif // ARDUINO_ARCH_RP2040
// Start IR Camera with basic data format
dfrIRPos.begin(DFROBOT_IR_IIC_CLOCK, DFRobotIRPositionEx::DataFormat_Basic, irSensitivity);
#ifdef USE_TINYUSB
// Initializing the USB devices chunk.
TinyUSBDevices.begin(2);
#endif
AbsMouse5.init(MouseMaxX, MouseMaxY, true);
// fetch the calibration data, other values already handled in ApplyInitialPrefs()
SelectCalPrefs(selectedProfile);
#ifdef USE_TINYUSB
// wait until device mounted
while(!USBDevice.mounted()) { yield(); }
#else
// was getting weird hangups... maybe nothing, or maybe related to dragons, so wait a bit
delay(100);
#endif
// IR camera maxes out motion detection at ~300Hz, and millis() isn't good enough
startIrCamTimer(IRCamUpdateRate);
// First boot sanity checks.
// Check if the initial profiles are blanked.
if(profileData[selectedProfile].xScale == 0) {
// SHIT, it's a first boot! Prompt to start calibration.
Serial.println("Preferences data is empty!");
SetMode(GunMode_CalCenter);
Serial.println("Pull the trigger to start your first calibration!");
while(!(buttons.pressedReleased == BtnMask_Trigger)) {
buttons.Poll(1);
buttons.Repeat();
}
} else {
// this will turn off the DotStar/RGB LED and ensure proper transition to Run
SetMode(GunMode_Run);
}
}
#if defined(ARDUINO_ARCH_RP2040) && defined(DUAL_CORE)
void setup1()
{
// Since we need this for the second core to activate, may as well give it something to do.
// initialize buttons (on the second core)
buttons.Begin();
}
#endif // ARDUINO_ARCH_RP2040
void startIrCamTimer(int frequencyHz)
{
#if defined(SAMCO_SAMD21)
startTimerEx(&TC4->COUNT16, GCLK_CLKCTRL_ID_TC4_TC5, TC4_IRQn, frequencyHz);
#elif defined(SAMCO_SAMD51)
startTimerEx(&TC3->COUNT16, TC3_GCLK_ID, TC3_IRQn, frequencyHz);
#elif defined(SAMCO_ATMEGA32U4)
startTimer3(frequencyHz);
#elif defined(SAMCO_RP2040)
rp2040EnablePWMTimer(0, frequencyHz);
irq_set_exclusive_handler(PWM_IRQ_WRAP, rp2040pwmIrq);
irq_set_enabled(PWM_IRQ_WRAP, true);
#endif
}
#if defined(SAMCO_SAMD21)
void startTimerEx(TcCount16* ptc, uint16_t gclkCtrlId, IRQn_Type irqn, int frequencyHz)
{
// use Generic clock generator 0
GCLK->CLKCTRL.reg = (uint16_t)(GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | gclkCtrlId);
while(GCLK->STATUS.bit.SYNCBUSY == 1); // wait for sync
ptc->CTRLA.bit.ENABLE = 0;
while(ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync
// Use the 16-bit timer
ptc->CTRLA.reg |= TC_CTRLA_MODE_COUNT16;
while(ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync
// Use match mode so that the timer counter resets when the count matches the compare register
ptc->CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ;
while(ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync
// Set prescaler
ptc->CTRLA.reg |= TIMER_TC_CTRLA_PRESCALER_DIV;
while(ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync
setTimerFrequency(ptc, frequencyHz);
// Enable the compare interrupt
ptc->INTENSET.reg = 0;
ptc->INTENSET.bit.MC0 = 1;
NVIC_EnableIRQ(irqn);
ptc->CTRLA.bit.ENABLE = 1;
while(ptc->STATUS.bit.SYNCBUSY == 1); // wait for sync
}
void TC4_Handler()
{
// If this interrupt is due to the compare register matching the timer count
if(TC4->COUNT16.INTFLAG.bit.MC0 == 1) {
// clear interrupt
TC4->COUNT16.INTFLAG.bit.MC0 = 1;
irPosUpdateTick = 1;
}
}
#endif // SAMCO_SAMD21
#if defined(SAMCO_SAMD51)
void startTimerEx(TcCount16* ptc, uint16_t gclkCtrlId, IRQn_Type irqn, int frequencyHz)
{
// use Generic clock generator 0
GCLK->PCHCTRL[gclkCtrlId].reg = GCLK_PCHCTRL_GEN_GCLK0 | GCLK_PCHCTRL_CHEN;
while(GCLK->SYNCBUSY.reg); // wait for sync
ptc->CTRLA.bit.ENABLE = 0;
while(ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync
// Use the 16-bit timer
ptc->CTRLA.reg |= TC_CTRLA_MODE_COUNT16;
while(ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync
// Use match mode so that the timer counter resets when the count matches the compare register
ptc->WAVE.bit.WAVEGEN = TC_WAVE_WAVEGEN_MFRQ;
while(ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync
// Set prescaler
ptc->CTRLA.reg |= TIMER_TC_CTRLA_PRESCALER_DIV;
while(ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync
setTimerFrequency(ptc, frequencyHz);
// Enable the compare interrupt
ptc->INTENSET.reg = 0;
ptc->INTENSET.bit.MC0 = 1;
NVIC_EnableIRQ(irqn);
ptc->CTRLA.bit.ENABLE = 1;
while(ptc->SYNCBUSY.bit.STATUS == 1); // wait for sync
}
void TC3_Handler()
{
// If this interrupt is due to the compare register matching the timer count
if(TC3->COUNT16.INTFLAG.bit.MC0 == 1) {
// clear interrupt
TC3->COUNT16.INTFLAG.bit.MC0 = 1;
irPosUpdateTick = 1;
}
}
#endif // SAMCO_SAMD51
#if defined(SAMCO_SAMD21) || defined(SAMCO_SAMD51)
void setTimerFrequency(TcCount16* ptc, int frequencyHz)
{
int compareValue = (F_CPU / (TIMER_PRESCALER_DIV * frequencyHz));
// Make sure the count is in a proportional position to where it was
// to prevent any jitter or disconnect when changing the compare value.
ptc->COUNT.reg = map(ptc->COUNT.reg, 0, ptc->CC[0].reg, 0, compareValue);
ptc->CC[0].reg = compareValue;
#if defined(SAMCO_SAMD21)
while(ptc->STATUS.bit.SYNCBUSY == 1);
#elif defined(SAMCO_SAMD51)
while(ptc->SYNCBUSY.bit.STATUS == 1);
#endif
}
#endif
#ifdef SAMCO_ATMEGA32U4
void startTimer3(unsigned long frequencyHz)
{
// disable comapre output mode
TCCR3A = 0;
//set the pre-scalar to 8 and set Clear on Compare
TCCR3B = (1 << CS31) | (1 << WGM32);
// set compare value
OCR3A = F_CPU / (8UL * frequencyHz);
// enable Timer 3 Compare A interrupt
TIMSK3 = 1 << OCIE3A;
}
// Timer3 compare A interrupt
ISR(TIMER3_COMPA_vect)
{
irPosUpdateTick = 1;
}
#endif // SAMCO_ATMEGA32U4
#ifdef ARDUINO_ARCH_RP2040
void rp2040EnablePWMTimer(unsigned int slice_num, unsigned int frequency)
{
pwm_config pwmcfg = pwm_get_default_config();
float clkdiv = (float)clock_get_hz(clk_sys) / (float)(65535 * frequency);
if(clkdiv < 1.0f) {
clkdiv = 1.0f;
} else {
// really just need to round up 1 lsb
clkdiv += 2.0f / (float)(1u << PWM_CH1_DIV_INT_LSB);
}
// set the clock divider in the config and fetch the actual value that is used
pwm_config_set_clkdiv(&pwmcfg, clkdiv);
clkdiv = (float)pwmcfg.div / (float)(1u << PWM_CH1_DIV_INT_LSB);
// calculate wrap value that will trigger the IRQ for the target frequency
pwm_config_set_wrap(&pwmcfg, (float)clock_get_hz(clk_sys) / (frequency * clkdiv));
// initialize and start the slice and enable IRQ