' ****************************************************************************** ' * Puff-eyes.bas * ' * Robot Control Firmware with MIDI-Input * ' * for Proton+ compiler on 18F2525 microchip controller * ' * Godfried-Willem Raes * ' ****************************************************************************** ' board description: MidiHub board, rev. 3, nov.2006 [batch produced 2010] ' 02.10.2010: Project start. Replacement for existing implementation on the ' pulse-pic's controlling the puff pipes. ' Programmer used: PicKIT2 (microchip) ' 04.10.2010: Hardware ready. ' First sketch for a real multitasker, required for smooth motor movement. ' 05.10.2010: Wiring finished. ' First tests: Eye lites and red light work well. ' Motor enable needs inversion... ' only right PIR seems to trigger right eye LED... ' in fact all motor control signals must be inverted, since that's what the mosfets does... ' 06.10.2010: The multitasker for periodic tasks should be operational now. ' First test on Puff: ok, but direction of rotation must be inverted. ' CC31 added for motor speed control. ' Calibration procedure rewritten. Now in 3 passes. ' This be firmware V1.0 - flashed. ' 07.10.2010: some bugs removed in CC90, algo 1 ' CC90, algo's 1,2,3,4 implemented. ' The double triggering problem with the Pepperl+Fuchs transducers has to be improved. ' Version V1.1 - flashed. ' Components to be controlled: 2 lights (eyes, white) ' left eye: X15-2, RC3 - pin 14 (Blue wire) ' right eye: X15-3 RC0 - pin 11 (Yellow wire) ' stepper motor (via controller board) X11 & X12 ' required signals: ' clock (pulse duration >= 10 microseconds, trig on negative edge) ' RB - X12-2 pin 21 ' RB - X12-3 pin 22 RED 1W LED (mounted on PC-board) ' direction RC4 - X11-2 pin 16 ' enable RC5 - X11-2 pin 15 ' 2 sensor inputs (analog)- position end sensors - ' left X16-3 (RA4 pin 5) ' right X16-4 (RA3 pin 4) ' 2 sensor inputs pyrodetectors left X16-1 (RA6 pin 7) ' right X16-2 (RA5 pin 6) ' rotating light (PWM) X17-2 ' option X17-3 Include "18F2525.inc" 'version for the puff-eyes board. 'Include "18F25K20.inc" 'for test & debug on an Amicus board. 'constant definitions: 'initialisations for the midi input parser: Symbol Midichannel = 13 ' Puff_Channel Symbol NoteOff_Status = 128 + Midichannel ' 2 bytes follow Symbol NoteOn_Status = 144 + Midichannel Symbol Keypres_Status = 160 + Midichannel Symbol Control_Status = 176 + Midichannel Symbol ProgChange_Status = 192 + Midichannel ' 1 byte message Symbol Aftertouch_Status = 208 + Midichannel ' 1 byte follows Symbol Pitchbend_Status = 224 + Midichannel ' lsb msb follow Symbol NrTasks = 8 ' maximum 16 , sofar we use only 2 tasks here. ' Setup the USART Declare Hserial_Baud = 31250 ' Set baud rate for the USART to MIDI specs. Declare Hserial_TXSTA = 0x24 ' instead of the normal 0x20 - ?? 0x24 ' Declare Hserial_Clear = On ' should clear on errors. Bytes get lost of course... This must be 31250 for MIDI ' Create variables Dim Cntl As TMR0L.Word 'this is the trick to read both TMR0L and TMR0H 'it makes Cntl the low word of cnt Dim Bytein As Byte System ' midi byte read from buffer Dim StBit As Bytein.7 ' highest bit of ByteIn Dim i As Byte System ' general purpose counter ' midi variables Dim statusbyte As Byte System Dim noteUit As Byte System ' note off + release value Dim release As Byte System Dim noteAan As Byte System ' note on + release value Dim velo As Byte System Dim notePres As Byte System ' note pressure + pressure value Dim pres As Byte System Dim Ctrl As Byte System ' continuous controller + value Dim value As Byte System Dim prog As Byte System ' program change + program-byte Dim aft As Byte System ' channel aftertouch Dim pblsb As Byte System ' pitch bend lsb Dim pbmsb As Byte System ' pitch bend msb Dim veltim As Dword System ' 32 bit velo Dim VelFlags As Word System ' bits 0 - 15 used as flags for active velo-timers Dim VelFlags0 As VelFlags.Byte0 ' alias for bits 0-7 Dim VelFlags1 As VelFlags.Byte1 ' bits 8-15 Dim CC66 As Byte System ' global on/off switch Dim st As Byte System Dim b1 As Byte System Dim b2 As Byte System Dim Cnt As Dword System ' 32 bit counter Dim MotPos As Byte System ' actual position of the motor Dim MinPos As Byte System ' extreme left position Dim MaxPos As Byte System ' extreme right position Dim CenterPos As Byte System ' central position Dim sollPos As Byte System ' set with controller 30 Dim CC90 As Byte System ' robotic mode: automation of motor and eyes with sensors Dim MotorPeriod As Word System ' motor speed, CC31 Dim LeftPirCnt As Byte System Dim RightPirCnt As Byte System Dim tmp As Word System '----------------------------------------------------------------------------------------- ' Load the USART Interrupt handler And buffer read subroutines into memory Include "Midi_Irq.inc" ' our own version for UART And Timer0 Interrupt Include "Timers.inc" ' required for velo support with timed pulses and periods. Include "DwordArrays.inc" ' support for dword arrays. 'first framework for a multitasker: Dim Task_rsi[NrTasks] As Word 'task reschedule interval (period), if 0 the task is not active 'not used here yet, since we use the MotorPeriod vaiable as rsi. Dim Velmsb[NrTasks] As Word 'the application for velo-timers, is in fact just a one-shot task Dim VelLsb[NrTasks] As Word DeclareDwordArray(TimeVals , NrTasks) 'alternative using the macro's. [not yet used] ' Mapping defines for midi-events on pin outputs and inputs: ' There are only 16 pins free: RB0-5, RC0, RC3-5, RA0-RA5 $define Eye_Left PORTC.3 ' left eye light front , pin 11 X15 $define Eye_Right PORTC.0 ' right eye light front, pin 14 X15 $define Motor_Enable PORTC.5 ' pin 16 X11-2 $define Motor_Dir PORTC.4 ' pin 15 X11-3 $define Motor_Clock PORTB.0 ' pin 21 X12-2 $define RedLite PORTB.1 ' pin 22 X12-3 $define PIR_Left PORTA.5 ' pin 7 X16-1 INPUT from PIR left $define PIR_Right PORTA.4 ' pin 6 X16-2 INPUT from PIR right $define PF_Left PORTA.3 ' pin 5 X16-3 INPUT from end sensor left $define PF_Right PORTA.2 ' pin 4 X16-4 INPUT from end sensor right $define PWM1 PORTC.1 ' PWM channel -RC1 HPWM1 X17 $define PWM2 PORTC.2 ' PWM channel -RC2 HPWM0 X17 'red LED for debug: $define Debug_Led PORTB.5 ' mapped on midi note 0 for testing ' configure the input and output pins: TRISA = %01111100 'bits set to 0 are output, 1 = input TRISB = %11100000 TRISC = %11000000 'RC1 en RC2 zijn pwm outputs and must be set to output 'RC6 en RC7 zijn USART I/O and must be set to input 'make sure we initialize those pins to 0 on start up: Low Eye_Left Low Eye_Right High Motor_Enable 'motor disabled at start High Motor_Dir Low Motor_Clock 'to make it high at rest on the controller input. Low RedLite Low Debug_Led Input PIR_Left Input PIR_Right Input PF_Left Input PF_Right '----------------------------------------------------------------------------------------- ' Main program starts here MAIN: High Debug_Led DelayMS 100 ' wait for stability Low Debug_Led Init_Usart_Interrupt ' Initiate the USART serial buffer interrupt ' this procedure is in the include file Clear_Serial_Buffer ' Clear the serial buffer and reset its pointers ' in the include as well ' following only required for pulse outputs: ' Note that we can only use timer0 since the other timers ' are used by the PWM subsystem ' Configure Timer0 for: ' Clear TMR0L and TMR0H registers ' Interrupt on Timer0 overflow ' 16-bit operation ' Internal clock source 40MHz ' 1:256 Prescaler : thus 40MHz / 256 = 156.250kHz Opentimer0 (Timer_INT_On & T0_16BIT & T0_SOURCE_INT & T0_PS_1_256) ' Setup the High priorities for the interrupts GoSub Init_Puff ' required initialisation ' start the main program loop: Start_Loop: ' Create an infinite loop Bytein = HRSIn ' Read data from the serial buffer, with no timeout ' Start the midi parser. Midi_Parse: If Bytein > ProgChange_Status Then ' was Pitchbend_Status Then , but here this is not implemented. If Bytein > 253 Then '254 = midiclock, 255= reset 'midiclock can interrupt all other msg's... '255 had to be intercepted since thats what we 'get when no new byte flows in (?) GoTo Midi_Parse_End 'throw away... Else Clear statusbyte 'reset the status byte GoTo Midi_Parse_End 'throw away End If EndIf If StBit =1 Then 'should be faster than If Bytein > 127 Then 'status byte received, bit 7 is set Clear statusbyte 'if on another channel, the statusbyte needs a reset Select Bytein 'eqv to Select case ByteIn Case NoteOff_Status statusbyte = Bytein noteUit = 255 'reset value. Cannot be 0 !!! release = 255 '0 is a valid midi note! Case NoteOn_Status statusbyte = Bytein noteAan = 255 velo = 255 Case Keypres_Status statusbyte = Bytein notePres = 255 pres = 255 Case Control_Status statusbyte = Bytein Ctrl = 255 value = 255 Case ProgChange_Status statusbyte = Bytein prog = 255 ' Case Aftertouch_Status ' statusbyte = Bytein ' aft = 255 ' Case Pitchbend_Status ' statusbyte = Bytein ' pblsb = 255 ' pbmsb = 255 End Select Else 'midi byte is 7 bits Select statusbyte Case 0 'not a message for this channel GoTo Midi_Parse_End 'disregard Case NoteOff_Status If noteUit = 255 Then noteUit = Bytein Else release = Bytein 'message complete, so we can do the action... Select noteUit Case 120 'red led light Low Redlite Case 121 Low Eye_Left Case 122 Low Eye_Right Case 123 GoSub NO6 Case 124 GoSub NO7 End Select noteUit = 255 'reset EndIf GoTo Midi_Parse_End Case NoteOn_Status If noteAan = 255 Then noteAan = Bytein Else velo = Bytein If velo = 0 Then Select noteAan Case 120 Low Redlite Case 121 Low Eye_Left Case 122 Low Eye_Right Case 123 GoSub NO6 Case 124 GoSub NO7 End Select noteAan = 255 'reset !!! GoTo Midi_Parse_End 'jump out EndIf Select noteAan Case 120 High Redlite Case 121 High Eye_Left Case 122 High Eye_Right Case 123 GoSub NA6 Case 124 GoSub NA7 Case Else noteAan = 255 GoTo Midi_Parse_End End Select noteAan = 255 'reset EndIf GoTo Midi_Parse_End Case Keypres_Status If notePres = 255 Then notePres = Bytein Else pres = Bytein GoSub KeyPres EndIf GoTo Midi_Parse_End Case Control_Status If Ctrl = 255 Then Ctrl = Bytein Else value = Bytein GoSub Controller EndIf GoTo Midi_Parse_End Case ProgChange_Status If prog = 255 Then 'single byte message prog = Bytein 'weak coding... GoSub ProgChange EndIf End Select EndIf Midi_Parse_End: 'jump out of parser label PIR_Sensors: Select CC90 Case 0 'jump out. Case 1 'integrator: 'LeftPIRCnt = (LeftPirCnt * 3) + PIR_Left 'LeftPirCnt = LeftPirCnt > 2 If PIR_Left =1 Then If LeftPirCnt < 255 Then Inc LeftPirCnt 'ceil to 255 Else If LeftPirCnt > 0 Then Dec LeftPirCnt 'ceil to 0 Else Low Eye_Left EndIf EndIf If PIR_Right =1 Then If RightPirCnt < 255 Then Inc RightPirCnt 'ceil to 255 Else If RightPirCnt > 0 Then Dec RightPirCnt Else Low Eye_Right EndIf EndIf tmp = 256 + RightPirCnt - LeftPirCnt 'tmp is word sollPos = tmp >> 1 'bring it back to byte range, = /2 If sollPos < MotPos Then High Eye_Left GoSub MoveLeft EndIf If sollPos > MotPos Then High Eye_Right GoSub MoveRight EndIf Case 2 'start fast sensor polling for body seeking mode: If PIR_Left & PIR_Right = 1 Then 'move to center position If MotPos < CenterPos Then sollPos = 128 GoSub MoveRight EndIf If MotPos > CenterPos Then sollPos = 128 GoSub MoveLeft EndIf High Eye_Left High Eye_Right Else If PIR_Left = 1 Then 'move left 1 step If MotPos > 0 Then sollPos = MotPos -1 GoSub MoveLeft High Eye_Left Low Eye_Right Else Low Eye_Left EndIf If PIR_Right = 1 Then 'move right 1 step If MotPos < 255 Then sollPos = MotPos + 1 GoSub MoveRight High Eye_Right Low Eye_Left Else Low Eye_Right EndIf EndIf Case 3 'cleverly coded integrator... fast version LeftPirCnt = LeftPirCnt >> 1 'shift right 1 position = / 2 LeftPirCnt.7 = PIR_Left 'set the new bit in the msb RightPirCnt = RightPirCnt >> 1 RightPirCnt.7 = PIR_Right 'both vars are now always confined to the 0-255 range tmp = 256 + RightPirCnt - LeftPirCnt 'tmp is word sollPos = tmp >> 1 'bring it back to byte range, = /2 Select sollPos Case < MotPos High Eye_Left GoSub MoveLeft Case > MotPos High Eye_Right GoSub MoveRight Case = MotPos High Eye_Right High Eye_Left End Select If LeftPirCnt = 0 Then Low Eye_Left If RightPirCnt = 0 Then Low Eye_Right Case 4 'clever integrator... relaxed version LeftPirCnt = LeftPirCnt << 1 'shift left 1 position = * 2 LeftPirCnt.0 = PIR_Left 'set the new bit in the lsb RightPirCnt = RightPirCnt << 1 RightPirCnt.0 = PIR_Right 'both vars are now always confined to the 0-255 range tmp = 256 + RightPirCnt - LeftPirCnt 'tmp is word sollPos = tmp >> 1 'bring it back to byte range, = /2 Select sollPos Case < MotPos High Eye_Left GoSub MoveLeft Case > MotPos High Eye_Right GoSub MoveRight Case = MotPos High Eye_Right High Eye_Left End Select If LeftPirCnt = 0 Then Low Eye_Left If RightPirCnt = 0 Then Low Eye_Right End Select 'end sensor polling Check_Timers: ' here we check the Task counters and compare them with the cnt value ' using the Velflags dword variable: If VelFlags > 0 Then 'if any bit is set here, there is a timer running Cnt.LowWord = Cntl 'read counter Clear i Repeat If GetBit VelFlags, i = 1 Then 'check which counter is active veltim.Word1 = Velmsb[i] veltim.Word0 = VelLsb[i] If Cnt >= veltim Then '32 bit compare On i GoSub Task0,Task1,Task2,Task3,Task4,Task5,Task6,Task7 'if we recharge the veltimers in these procs, we get 'periodic timers, or the base of a multitasking system. EndIf EndIf Inc i Until i = NrTasks EndIf GoTo Start_Loop 'end of the main loop '-------------------------------------- NO6: HPWM 1, 0, 3906 Low PWM1 Return NO7: HPWM 0, 0, 3906 Low PWM2 Return '-------------------------------------- ' note-ON procedures: NA6: If velo < 127 Then HPWM 1, velo + velo, 3906 Else High PWM1 EndIf Return NA7: If velo < 127 Then HPWM 0, velo + velo, 3906 Else High PWM2 EndIf Return KeyPres: 'the note to which the pressure should be applied is passed in NotePres, the value in Pres 'modulate the loudness of playing notes. notePres = 255 Return ProgChange: prog = 255 'this is not realy required Return Pitchbend: 'only implemented on dsPIC based robots pblsb = 255 Return Aftertouch: 'this is the channel aftertouch, affecting all notes aft = 255 'not mandatory Return Controller: Select Ctrl Case 30 sollPos = value + value '0-254 If sollPos < MotPos Then Clear VelFlags.1 'make sure there is no task active for the other direction... 'move left GoSub MoveLeft Ctrl = 255 Return EndIf If sollPos > MotPos Then 'move right Clear VelFlags.0 GoSub MoveRight Ctrl = 255 Return EndIf Case 31 'can be used to change the speed of the motor MotorPeriod = 2048 - (value << 4) '= value * 16 Ctrl = 255 Return Case 66 'on/off for robot If value = 0 Then GoSub AllNotesOff EndIf CC66 = value 'GoTo Ctrl_Parse_End Case 67 'calibration commands: run in a row, with due time in between! If value = 64 Then GoSub CalibrateLeft EndIf 'Goto Ctrl_Parse_End Case 68 If value = 64 Then GoSub CalibrateRight EndIf Case 69 If value = 64 Then GoSub CalibrateCenter EndIf Case 90 CC90 = value 'robotic sensor modes If value > 0 Then Low Motor_Enable 'inverted!!!, this this enables the motor EndIf Case 123 'all notes off GoSub AllNotesOff End Select Ctrl_Parse_End: Ctrl = 255 'mandatory reset Return AllNotesOff: Low Eye_Right Low Eye_Left High Motor_Enable 'ínverted! - so this is disabled. Low Redlite Low PWM1 Low PWM2 Low Motor_Clock 'inverted! Clear CC90 Clear VelFlags Return Init_Puff: Clear CC90 MinPos = 0 MaxPos = 255 CenterPos = 128 MotorPeriod = 1024 Low Eye_Right Low Eye_Left High Motor_Enable 'unpower motor Low Redlite Low PWM1 Low PWM2 Low Motor_Clock Clear VelFlags 'no timer active on start up Return MoveLeft: If PF_Left = 0 Then 'check endsensor Low Motor_Enable 'motor should become enabled now High Motor_Dir 'load task0: Set VelFlags.0 Cnt.LowWord = Cntl 'read timer veltim = Cnt + MotorPeriod 'add the period duration for the motor pulses Velmsb[0] = veltim.Word1 VelLsb[0] = veltim.Word0 PulseOut Motor_Clock, 10, High If MotPos > 0 Then Dec MotPos Else 'endsensor reached MinPos = MotPos sollPos = MotPos High Motor_Enable 'unpower, disabled Clear VelFlags.0 EndIf Return MoveRight: If PF_Right = 0 Then 'make sure we are not at the end. Low Motor_Enable 'inverted: 0 enables the motor Low Motor_Dir 'direction of rotation 'load task1: Set VelFlags.1 Cnt.LowWord = Cntl 'read timer veltim = Cnt + MotorPeriod 'add the period duration for the motor pulses Velmsb[1] = veltim.Word1 VelLsb[1] = veltim.Word0 PulseOut Motor_Clock, 10, High If MotPos < 255 Then Inc MotPos Else 'endsensor was reached... MaxPos = MotPos sollPos = MotPos Clear VelFlags.1 High Motor_Enable 'disable motor power End If Return CalibrateLeft: 'callibration procedure to calculate the center position 'the motor should turn left till the end, than right till the end and 'finally return the the central position sollPos = 0 Set VelFlags.0 Clear VelFlags.1 GoSub MoveLeft Return CalibrateRight: sollPos = 255 Set VelFlags.1 Clear VelFlags.0 GoSub MoveRight CenterPos = (MaxPos - MinPos) << 1 Return CalibrateCenter: CenterPos = (MaxPos - MinPos) >> 1 Return Task0: 'called when a timer runs out 'recharge the timer, if no stop flag is set. ' by incrementing or decrementing the rsi (reschedule interval), we can 'make gentle speedups and slowdowns for motors, pwm's and fades. 'turn left task for eye motor: If MotPos = sollPos Then Clear VelFlags.0 'stop task, as position is reached Return End If If PF_Left = 0 Then 'reload task0: Set VelFlags.0 'can just stay set Cnt.LowWord = Cntl 'read timer veltim = Cnt + MotorPeriod 'add the period duration for the motor pulses Velmsb[0] = veltim.Word1 VelLsb[0] = veltim.Word0 PulseOut Motor_Clock, 10, High If MotPos > 0 Then Dec MotPos Else 'endsensor is reached MinPos = MotPos sollPos = MotPos High Motor_Enable 'unpower Clear VelFlags.0 EndIf Return Task1: 'period in Task_rsi[1] 'turn motor to the right: If MotPos = sollPos Then Clear VelFlags.1 Return EndIf If PF_Right = 0 Then 'check the sensor, is it off? 'RELOAD TASK1: Set VelFlags.1 Cnt.LowWord = Cntl 'read timer veltim = Cnt + MotorPeriod 'add the period duration for the motor pulses Velmsb[1] = veltim.Word1 VelLsb[1] = veltim.Word0 PulseOut Motor_Clock, 10, High If MotPos < 255 Then Inc MotPos Else 'endsensor reached MaxPos = MotPos sollPos = MotPos Clear VelFlags.1 High Motor_Enable EndIf Return Task2: 'period in Task_rsi[2] 'sofar, only 2 tasks implemented here. Return Task3: Return Task4: Return Task5: Return Task6: Return Task7: Return '[EOF]