It's taken a little while to get the time to draw up a "proper" schematic and PCB for the controller board, but - after a few requests in recent days - here they are:
(this is a press-n-peel ready version of the controller board; although when transferred the lettering will appear the wrong way around, this tells you that you got it right!)
The PCB is not a perfect final version- as can be seen from the massive number of wire jumpers and zero ohm resistors. Both are essentially the same thing, but 0R resistors are simply "scars" from many, quick changes the design - implemented after the schematic and PCB we originally drawn up - there's nothing like iterative prototyping!
This is how the board looks when it's all made up and plugged in:
(we've created a little "daughter board" with some jog buttons on it, to avoid having to keep poking wires into holes to manually position the xy bed!)
Since we don't want to stop the machine from moving while data is being received, we've re-created the kind of thing you might find in an Arduino serial library (though minus the blocking functions for reading the serial buffer). It's basically an interrupt-driven circular buffer and it works like this;
- We're using the hardware UART for receiving serial data.
- We set up a high priority interrupt to fire every time a character is received in via the serial port.
- The character is added to a buffer (a 34 character byte array)
- If the byte received has the value zero, set a flag to say "we've had a new message".
- In the main code, periodically check the data received flag and parse the new message as necessary
Keeping track of positions in the buffer is on the only really tricky thing here.
We need to use two "pointers" to track two position in the array - the first pointer is the place where we want to add the next character that arrives via serial. The second pointer is the place from where we should start reading data when we want to pull the message back from the buffer
- Whenever a character is received over serial, the interrupt fires which does a few things;
- Writes the received character/byte into the circular buffer
- Moves the "write pointer" along one place
- If the received byte is zero, sets the "message received" flag
When the "message received" flag is set, in our main program code (not in the interrupt - it's important to keep that code as tight as possible) we read the data back out from the buffer/byte array. This approach means that we're not "locking" the serial buffer while we're reading from it - in fact, it's quite possible that we can be reading the serial buffer while appending data to the end of it (via the interrupt routine) at the same time!
When we have read back a zero value byte from the buffer, we know we've reached the end of the message. If we reach the end of the buffer and both the "reading" and "writing" pointers are looking at the same space, we know that the serial buffer is empty so we clear the "message received" flag.
If the either pointer exceeds the size of our buffer, it is re-set to zero - effectively "wrapping round" to the start of the array again (and thus creating a "circular" buffer).
Our buffer size is 34 characters so, of course, if we stream in more than 34 bytes without a zero value (to trigger the "read back" routine in the main code and update the read pointer) there's a good chance that the write pointer will "overtake" the read pointer. When this happens, the data read back from the buffer will be truncated. So it's important to keep serial messages down to less than 34 characters.
We settled on 34 as this allows us to stream an entire message to the 16x2 character display (32 characters) if necessary, and still have a couple of bytes over, in the buffer. Most of our serial messages are much shorter than this (a couple of bytes at most).
Because we're using zero as an end of message marker, it means that we need to use a different method to send actual numerical data (since the numerical value zero might actually be a valid part of that data). We don't want to send the legitimate numerical value zero over serial and have our controller interpret it as an end of message marker! For this reason, we're converting our numerical values into hex, and sending them as ASCII characters.
So to send the value 255, for example, we send the character sequence "FF"
Similarly, to send the value 0, we send the character sequence "00".
So to send a byte value zero, we actually send two bytes, each of value 48 (the number zero is 48 in ascii). At the receiving end we convert the ascii characters into 0-9 or A-F, assign them their correct value (so the character "1" has the value 1, "A" has the value ten and so on) and apply to either the top or bottom "nibble" (4 bits) of the value transmitted, to create the actual numerical value originally sent.
This ensures that all numerical data has the value 48-59 or 65-70, and so we can still use our zero value to indicate the end of a serial message.
Define CONFIG1H = 0x0c
Define CONFIG2L = 0x18
Define CONFIG2H = 0x1e
Define CONFIG3L = 0x00
Define CONFIG3H = 0x01
Define CONFIG4L = 0x80
Define CONFIG4H = 0x00
Define CONFIG5L = 0x0f
Define CONFIG5H = 0xc0
Define CONFIG6L = 0x0f
Define CONFIG6H = 0xe0
Define CONFIG7L = 0x0f
Define CONFIG7H = 0x40
Define CLOCK_FREQUENCY = 20
AllDigital
declarations:
Dim state As Byte
Dim serial_read_pos As Byte
Dim serial_place_pos As Byte
Dim serial_buffer(35) As Byte
Dim serial_has_data As Bit
Dim serial_byte As Byte
Dim serial_state As Byte
Dim get_more_data As Bit
Dim lcd_char_count As Byte
Dim target_x As Long
Dim target_y As Long
Dim current_x As Long
Dim current_y As Long
Dim step_difference_x As Long
Dim step_difference_y As Long
Dim step_delay_x As Byte
Dim step_delay_y As Byte
Dim tmp_long As Long
Dim b_long As Long
Dim x_motor_state As Byte
Dim y_motor_state As Byte
Dim home_at_start As Bit
Dim is_moving As Bit
Dim is_jogging As Bit
Dim motor_dir_y As Bit
Dim motor_dir_x As Bit
Dim servo_dir As Bit
Dim servo_counter As Byte
Const state_default = 0
Const state_home = 1
Const serial_first_character = 0
Const serial_lcd_msg = 1
Const serial_get_position = 2
Const serial_vac_pen = 3
Const serial_servo = 4
Define LCD_BITS = 4
Define LCD_DREG = PORTD
Define LCD_DBIT = 4
Define LCD_RSREG = PORTD
Define LCD_RSBIT = 2
Define LCD_EREG = PORTD
Define LCD_EBIT = 3
Define LCD_RWREG = PORTD
Define LCD_RWBIT = 1
Dim i As Byte
Dim j As Byte
Dim k As Byte
symbols:
Symbol x_motor_enable = PORTA.2
Symbol x_coil_a_1 = PORTE.0
Symbol x_coil_a_2 = PORTA.5
Symbol x_coil_b_1 = PORTA.3
Symbol x_coil_b_2 = PORTA.4
Symbol y_motor_enable = RE.1
Symbol y_coil_a_1 = PORTC.2
Symbol y_coil_a_2 = PORTC.1
Symbol y_coil_b_1 = PORTE.2
Symbol y_coil_b_2 = PORTC.0
Symbol tx = PORTC.6
Symbol rx = PORTC.7
Symbol limit_home_x = PORTB.6
Symbol limit_home_y = PORTB.5
Symbol limit_extent_x = PORTC.5
Symbol limit_extent_y = PORTC.4
Symbol jog_left = PORTB.0
Symbol jog_right = PORTB.1
Symbol jog_up = PORTB.2
Symbol jog_down = PORTB.3
Symbol jog_slow = PORTB.4
Symbol servo_pin = PORTD.0
Symbol led_pin = PORTA.0
Symbol vac_pen_relay = PORTA.1
init:
ConfigPin PORTA = Output
ConfigPin PORTB = Input
ConfigPin PORTC = Output
ConfigPin PORTD = Output
ConfigPin PORTE = Output
ConfigPin tx = Output
ConfigPin rx = Input
ConfigPin servo_pin = Output
ConfigPin led_pin = Output
ConfigPin vac_pen_relay = Output
ConfigPin limit_extent_x = Input
ConfigPin limit_extent_y = Input
'------------------------------
'first make sure motors are off
'------------------------------
Gosub stop_x_motor
Gosub stop_y_motor
is_moving = 0
is_jogging = 0
'------------------------------------
'lift the servo up (pen is retracted)
'------------------------------------
servo_dir = 0
servo_counter = 0
'-----------------------------
'start with the vacuum pen off
'-----------------------------
Low vac_pen_relay
'---------------------
'start the UART/serial
'---------------------
Hseropen 9600 '115200
'create an interrupt on serial input
PIE1.RCIE = 1
IPR1.RCIP = 1 'rx is high priority
'set up the serial "cirular buffer"
serial_read_pos = 0
serial_place_pos = 0
For i = 0 To 34
serial_buffer(i) = 0
Next i
'-------------------------
'set up the stepper motors
'-------------------------
x_motor_state = 0
y_motor_state = 0
home_at_start = 0
target_x = 0
target_y = 0
current_x = 0
current_y = 0
'to use RC4 and RC5 we have to actively disable the USB
UCON.3 = 0
UCFG.3 = 1
'for RA2 we need to disconnect the vref comparitor
CVRCON.CVROE = 0
CVRCON.CVREN = 0
CVRCON.CVRSS = 0
'--------------
'set up the LCD
'--------------
High led_pin
Lcdinit 0
Lcdcmdout LcdClear
Lcdout " El Cheapo "
Lcdcmdout LcdLine2Home
WaitMs 1000
Low led_pin
Lcdout " Pick and Place "
Hserout "El Cheapo Ready", CrLf
'------------------------
'enable pull-ups on PORTB
'------------------------
INTCON2.RBPU = 0
Low led_pin
'---------------------------
'set up the state machine(s)
'---------------------------
state = state_home
If home_at_start = 0 Then state = state_default
serial_state = serial_first_character
Gosub set_servo
'---------------------------------------
'enable global and peripheral interrupts
'---------------------------------------
INTCON.GIE = 1
INTCON.PEIE = 1
loop:
Select Case state
'--------------
Case state_home
'--------------
target_x = 0
target_y = 0
current_x = 0
current_y = 0
If limit_home_x <> 0 Then current_x = 1
If limit_home_y <> 0 Then current_y = 1
If current_x = 0 And current_y = 0 Then
'send a message back to the host to tell them we're at home
Hserout "HOME", CrLf
'we're at the home position, so go to default state
state = state_default
Else
'move home as quickly as possible
step_delay_x = 2
step_delay_y = 2
Endif
'-----------------
Case state_default
'-----------------
'here we're waiting for the serial buffer to be flushed
'(when a zero byte is sent to indicate end of line)
'and when it is, parse the data out of it (because it's sent
'via ascii)
If serial_has_data = 1 Then
'parse the data from out of the buffer
serial_state = serial_first_character
get_more_data = 1
While get_more_data = 1
serial_byte = serial_buffer(serial_read_pos)
serial_read_pos = serial_read_pos + 1
If serial_read_pos > 34 Then serial_read_pos = 0
If serial_byte = 0 Then
'this is the message termination byte
If serial_read_pos = serial_place_pos Then
'the last character received was this termination character
'so we've emptied the serial buffer
serial_has_data = 0
Else
'we've received more data following the termination character
'so once we've finished with this message, leave the has_data
'flag set and we'll parse the next lot of data
Endif
get_more_data = 0
Else
'read the data from the buffer, one byte at a time
Select Case serial_state
'--------------------------
Case serial_first_character
'--------------------------
'we've just had our first serial character: this should be the
'message type marker
If serial_byte = "M" Then
'this is a string message
serial_state = serial_lcd_msg
lcd_char_count = 0
Lcdcmdout LcdClear
Endif
If serial_byte = "P" Then
'this is positional data, sent as two lots of
'four character hex values (16-bit)
lcd_char_count = 0 'use this to count the serial bytes in
tmp_long = 0
is_moving = 1
serial_state = serial_get_position
Endif
If serial_byte = "V" Then
'this is the vacuum pen command, next byte
'will tell us whether to turn it on or off
serial_state = serial_vac_pen
Endif
If serial_byte = "S" Then
'this is the servo up/down command
serial_state = serial_servo
Endif
'------------------
Case serial_lcd_msg
'------------------
'this is a character to display on the LCD
Lcdout serial_byte
lcd_char_count = lcd_char_count + 1
If lcd_char_count = 16 Then Lcdcmdout LcdLine2Home
If lcd_char_count = 32 Then
Lcdcmdout LcdHome
lcd_char_count = 0
Endif
'-----------------------
Case serial_get_position
'-----------------------
If lcd_char_count < 4 Then
'this is another hex-based character for the x position
Gosub hex_to_value
tmp_long = ShiftLeft(tmp_long, 4)
tmp_long = tmp_long Or b_long
If lcd_char_count = 3 Then
target_x = tmp_long
tmp_long = 0
Endif
Endif
If lcd_char_count >= 4 And lcd_char_count < 8 Then
Gosub hex_to_value
tmp_long = ShiftLeft(tmp_long, 4)
tmp_long = tmp_long Or b_long
If lcd_char_count = 7 Then
target_y = tmp_long
'draw the received values to the LCD
Gosub draw_coords_target
Endif
Endif
lcd_char_count = lcd_char_count + 1
'------------------
Case serial_vac_pen
'------------------
If serial_byte = 49 Or serial_byte = 1 Then
'this is the "on" command (ascii value one)
High vac_pen_relay
Lcdcmdout LcdHome
Lcdout "Vac pen on "
Else
Low vac_pen_relay
Lcdcmdout LcdHome
Lcdout "Vac pen off "
Endif
'----------------
Case serial_servo
'----------------
If serial_byte = 49 Or serial_byte = 1 Then
'this is the "servo down" command (ascii value one)
servo_dir = 1
Lcdcmdout LcdHome
Lcdout "Pen down "
Else
'if you've not specifically put the pen down, lift it up
servo_dir = 0
Lcdcmdout LcdHome
Lcdout "Pen up "
Endif
Gosub set_servo
EndSelect
Endif
Wend
Endif
'as well as waiting for serial data, we can always respond to button presses
If jog_left = 0 And current_x > 0 Then
is_jogging = 1
motor_dir_x = 0
target_x = current_x - 1
Endif
If jog_right = 0 Then
is_jogging = 1
motor_dir_x = 1
target_x = current_x + 1
Endif
If jog_up = 0 And current_y > 0 Then
is_jogging = 1
motor_dir_y = 0
target_y = current_y - 1
Endif
If jog_down = 0 Then
is_jogging = 1
motor_dir_y = 1
target_y = current_y + 1
Endif
EndSelect
'------------------------------------------------------------------------
'if the motors need to be turned, move them in steps.
'This takes a bit of thinking about: we set our target x and y values
'as "number of HALF-steps" from current position. So if we want to target
'a point 5 steps away, we set the target_x to 10.
'Every half step, reduce the current_x so if we make a whole step, we
'need to reduce current_x by 2 (not by one)
'------------------------------------------------------------------------
If target_x < current_x Then
'we need to step closer to our target in the X axis
'provided the limit switch hasn't been hit
If limit_home_x = 0 Then
'we've hit the home limit switch, so set current and target x to zero
current_x = 0
target_x = 0
is_moving = 0
Hserout "LIMIT X HOME", CrLf
Lcdcmdout LcdHome
Lcdout " "
Lcdcmdout LcdHome
Lcdout "Home X reached "
Else
motor_dir_x = 0
step_difference_x = current_x - target_x
Gosub set_x_stepper_speed
Gosub move_x_stepper
current_x = current_x - 1
Endif
Endif
If target_x > current_x Then
'we need to step closer to our target in the X axis
If limit_extent_x = 0 Then
'k = 1
'If k = 2 Then
target_x = current_x
is_moving = 0
Hserout "LIMIT X EXTENT", CrLf
Lcdcmdout LcdHome
Lcdout " "
Lcdcmdout LcdHome
Lcdout "Limit X reached "
Else
motor_dir_x = 1
step_difference_x = target_x - current_x
Gosub set_x_stepper_speed
Gosub move_x_stepper
current_x = current_x + 1
Endif
Endif
If target_y < current_y Then
'we need to step closer to our target in the Y axis
'provided the limit switch hasn't been hit
If limit_home_y = 0 Then
'we've hit the home limit switch, so set current and target x to zero
current_y = 0
target_y = 0
Hserout "LIMIT Y HOME", CrLf
Lcdcmdout LcdHome
Lcdout " "
Lcdcmdout LcdHome
Lcdout "Home Y reached "
Else
motor_dir_y = 0
step_difference_y = current_y - target_y
Gosub set_y_stepper_speed
Gosub move_y_stepper
current_y = current_y - 1
Endif
Endif
If target_y > current_y Then
'we need to step closer to our target in the Y axis
If limit_extent_y = 0 Then
'k = 1
'If k = 2 Then
target_y = current_y
Hserout "LIMIT Y EXTENT", CrLf
Lcdcmdout LcdHome
Lcdout " "
Lcdcmdout LcdHome
Lcdout "Limit Y reached "
Else
motor_dir_y = 1
step_difference_y = target_y - current_y
Gosub set_y_stepper_speed
Gosub move_y_stepper
current_y = current_y + 1
Endif
Endif
'--------------------------------------------------------------
'this just helps stop the motors getting too hot so they're not
'always powered, when they are stationary
'--------------------------------------------------------------
If target_x = current_x And jog_left = 1 And jog_right = 1 Then Gosub stop_x_motor
If target_y = current_y And jog_up = 1 And jog_down = 1 Then Gosub stop_y_motor
'----------------------------------------------------------------
'if we've just stopped moving after reaching a target point, send
'a message back via serial to the host, to say we've arrived
'----------------------------------------------------------------
If is_moving = 1 Then
If target_x = current_x And target_y = current_y Then
is_moving = 0
Hserout "READY", CrLf
Gosub draw_coords_current
Endif
Endif
'-----------------------------------------------------------------------
'if we've just released one of the jog buttons, write the current XY out
'-----------------------------------------------------------------------
If is_jogging = 1 Then
If jog_left = 1 And jog_right = 1 And jog_up = 1 And jog_down = 1 Then
is_jogging = 0
Gosub draw_coords_current
Endif
Endif
Goto loop
End
On Low Interrupt
Resume
On High Interrupt
If PIR1.RCIF = 1 Then
'we've just received a byte from the serial port
High led_pin
serial_byte = RCREG
serial_buffer(serial_place_pos) = serial_byte
serial_place_pos = serial_place_pos + 1
If serial_place_pos > 34 Then serial_place_pos = 0
If serial_byte = 0 Then
'set the flag to say we've a full message in the serial buffer
serial_has_data = 1
Low led_pin
Endif
Endif
Resume
move_x_stepper:
High x_motor_enable
If motor_dir_x = 1 Then
x_motor_state = x_motor_state + 1
Else
If x_motor_state = 0 Then
x_motor_state = 3
Else
x_motor_state = x_motor_state - 1
Endif
Endif
If x_motor_state > 3 Then x_motor_state = 0
'once each step, the polarity of one coil should be reversed
Select Case x_motor_state
Case 0
Low x_coil_a_2
Low x_coil_b_2
High x_coil_b_1
High x_coil_a_1
Case 1
Low x_coil_a_1
Low x_coil_b_2
High x_coil_b_1
High x_coil_a_2
Case 2
Low x_coil_a_1
Low x_coil_b_1
High x_coil_b_2
High x_coil_a_2
Case 3
Low x_coil_a_2
Low x_coil_b_1
High x_coil_b_2
High x_coil_a_1
EndSelect
WaitMs step_delay_x
Return
stop_x_motor:
Low x_coil_a_1
Low x_coil_a_2
Low x_coil_b_1
Low x_coil_b_2
Low x_motor_enable
Return
move_y_stepper:
High y_motor_enable
If motor_dir_y = 1 Then
y_motor_state = y_motor_state + 1
Else
If y_motor_state = 0 Then
y_motor_state = 3
Else
y_motor_state = y_motor_state - 1
Endif
Endif
If y_motor_state > 3 Then y_motor_state = 0
'once each step, the polarity of one coil should be reversed
Select Case y_motor_state
Case 0
Low y_coil_a_2
Low y_coil_b_2
High y_coil_b_1
High y_coil_a_1
Case 1
Low y_coil_a_1
Low y_coil_b_2
High y_coil_b_1
High y_coil_a_2
Case 2
Low y_coil_a_1
Low y_coil_b_1
High y_coil_b_2
High y_coil_a_2
Case 3
Low y_coil_a_2
Low y_coil_b_1
High y_coil_b_2
High y_coil_a_1
EndSelect
WaitMs step_delay_y
Return
stop_y_motor:
Low y_coil_a_1
Low y_coil_a_2
Low y_coil_b_1
Low y_coil_b_2
Low y_motor_enable
Return
hex_to_value:
'ascii zero is 48, ascii A is 65
If serial_byte >= 65 Then
'if ascii A is 65, we want A=10, B=11 etc.
b_long = serial_byte - 55
Else
b_long = serial_byte - 48
Endif
Return
set_x_stepper_speed:
step_delay_x = 2
If step_difference_x < 20 Then step_delay_x = 8
If step_difference_x < 10 Then step_delay_x = 14
If step_difference_x < 4 Then step_delay_x = 25
If jog_left = 0 Or jog_right = 0 Then step_delay_x = 2
If jog_slow = 0 Then step_delay_x = 50
Return
set_y_stepper_speed:
step_delay_y = 2
If step_difference_y < 20 Then step_delay_y = 8
If step_difference_y < 10 Then step_delay_y = 14
If step_difference_y < 4 Then step_delay_y = 25
If jog_up = 0 Or jog_down = 0 Then step_delay_y = 2
If jog_slow = 0 Then step_delay_y = 50
Return
draw_coords_target:
Lcdcmdout LcdClear
Lcdout "Target position:"
Lcdcmdout LcdLine2Home
Lcdout "X:", #target_x, " "
Lcdout "Y:", #target_y
Return
draw_coords_current:
Lcdcmdout LcdClear
Lcdout "Current position"
Lcdcmdout LcdLine2Home
Lcdout "X:", #current_x, " "
Lcdout "Y:", #current_y
Return
set_servo:
'this is a quick and dirty blocking function
High led_pin
For j = 0 To 50
If servo_dir = 0 Then
ServoOut servo_pin, 150
Else
ServoOut servo_pin, 200
Endif
WaitMs 20
Next j
Low led_pin
Return
Define CONFIG2L = 0x18
Define CONFIG2H = 0x1e
Define CONFIG3L = 0x00
Define CONFIG3H = 0x01
Define CONFIG4L = 0x80
Define CONFIG4H = 0x00
Define CONFIG5L = 0x0f
Define CONFIG5H = 0xc0
Define CONFIG6L = 0x0f
Define CONFIG6H = 0xe0
Define CONFIG7L = 0x0f
Define CONFIG7H = 0x40
Define CLOCK_FREQUENCY = 20
AllDigital
declarations:
Dim state As Byte
Dim serial_read_pos As Byte
Dim serial_place_pos As Byte
Dim serial_buffer(35) As Byte
Dim serial_has_data As Bit
Dim serial_byte As Byte
Dim serial_state As Byte
Dim get_more_data As Bit
Dim lcd_char_count As Byte
Dim target_x As Long
Dim target_y As Long
Dim current_x As Long
Dim current_y As Long
Dim step_difference_x As Long
Dim step_difference_y As Long
Dim step_delay_x As Byte
Dim step_delay_y As Byte
Dim tmp_long As Long
Dim b_long As Long
Dim x_motor_state As Byte
Dim y_motor_state As Byte
Dim home_at_start As Bit
Dim is_moving As Bit
Dim is_jogging As Bit
Dim motor_dir_y As Bit
Dim motor_dir_x As Bit
Dim servo_dir As Bit
Dim servo_counter As Byte
Const state_default = 0
Const state_home = 1
Const serial_first_character = 0
Const serial_lcd_msg = 1
Const serial_get_position = 2
Const serial_vac_pen = 3
Const serial_servo = 4
Define LCD_BITS = 4
Define LCD_DREG = PORTD
Define LCD_DBIT = 4
Define LCD_RSREG = PORTD
Define LCD_RSBIT = 2
Define LCD_EREG = PORTD
Define LCD_EBIT = 3
Define LCD_RWREG = PORTD
Define LCD_RWBIT = 1
Dim i As Byte
Dim j As Byte
Dim k As Byte
symbols:
Symbol x_motor_enable = PORTA.2
Symbol x_coil_a_1 = PORTE.0
Symbol x_coil_a_2 = PORTA.5
Symbol x_coil_b_1 = PORTA.3
Symbol x_coil_b_2 = PORTA.4
Symbol y_motor_enable = RE.1
Symbol y_coil_a_1 = PORTC.2
Symbol y_coil_a_2 = PORTC.1
Symbol y_coil_b_1 = PORTE.2
Symbol y_coil_b_2 = PORTC.0
Symbol tx = PORTC.6
Symbol rx = PORTC.7
Symbol limit_home_x = PORTB.6
Symbol limit_home_y = PORTB.5
Symbol limit_extent_x = PORTC.5
Symbol limit_extent_y = PORTC.4
Symbol jog_left = PORTB.0
Symbol jog_right = PORTB.1
Symbol jog_up = PORTB.2
Symbol jog_down = PORTB.3
Symbol jog_slow = PORTB.4
Symbol servo_pin = PORTD.0
Symbol led_pin = PORTA.0
Symbol vac_pen_relay = PORTA.1
init:
ConfigPin PORTA = Output
ConfigPin PORTB = Input
ConfigPin PORTC = Output
ConfigPin PORTD = Output
ConfigPin PORTE = Output
ConfigPin tx = Output
ConfigPin rx = Input
ConfigPin servo_pin = Output
ConfigPin led_pin = Output
ConfigPin vac_pen_relay = Output
ConfigPin limit_extent_x = Input
ConfigPin limit_extent_y = Input
'------------------------------
'first make sure motors are off
'------------------------------
Gosub stop_x_motor
Gosub stop_y_motor
is_moving = 0
is_jogging = 0
'------------------------------------
'lift the servo up (pen is retracted)
'------------------------------------
servo_dir = 0
servo_counter = 0
'-----------------------------
'start with the vacuum pen off
'-----------------------------
Low vac_pen_relay
'---------------------
'start the UART/serial
'---------------------
Hseropen 9600 '115200
'create an interrupt on serial input
PIE1.RCIE = 1
IPR1.RCIP = 1 'rx is high priority
'set up the serial "cirular buffer"
serial_read_pos = 0
serial_place_pos = 0
For i = 0 To 34
serial_buffer(i) = 0
Next i
'-------------------------
'set up the stepper motors
'-------------------------
x_motor_state = 0
y_motor_state = 0
home_at_start = 0
target_x = 0
target_y = 0
current_x = 0
current_y = 0
'to use RC4 and RC5 we have to actively disable the USB
UCON.3 = 0
UCFG.3 = 1
'for RA2 we need to disconnect the vref comparitor
CVRCON.CVROE = 0
CVRCON.CVREN = 0
CVRCON.CVRSS = 0
'--------------
'set up the LCD
'--------------
High led_pin
Lcdinit 0
Lcdcmdout LcdClear
Lcdout " El Cheapo "
Lcdcmdout LcdLine2Home
WaitMs 1000
Low led_pin
Lcdout " Pick and Place "
Hserout "El Cheapo Ready", CrLf
'------------------------
'enable pull-ups on PORTB
'------------------------
INTCON2.RBPU = 0
Low led_pin
'---------------------------
'set up the state machine(s)
'---------------------------
state = state_home
If home_at_start = 0 Then state = state_default
serial_state = serial_first_character
Gosub set_servo
'---------------------------------------
'enable global and peripheral interrupts
'---------------------------------------
INTCON.GIE = 1
INTCON.PEIE = 1
loop:
Select Case state
'--------------
Case state_home
'--------------
target_x = 0
target_y = 0
current_x = 0
current_y = 0
If limit_home_x <> 0 Then current_x = 1
If limit_home_y <> 0 Then current_y = 1
If current_x = 0 And current_y = 0 Then
'send a message back to the host to tell them we're at home
Hserout "HOME", CrLf
'we're at the home position, so go to default state
state = state_default
Else
'move home as quickly as possible
step_delay_x = 2
step_delay_y = 2
Endif
'-----------------
Case state_default
'-----------------
'here we're waiting for the serial buffer to be flushed
'(when a zero byte is sent to indicate end of line)
'and when it is, parse the data out of it (because it's sent
'via ascii)
If serial_has_data = 1 Then
'parse the data from out of the buffer
serial_state = serial_first_character
get_more_data = 1
While get_more_data = 1
serial_byte = serial_buffer(serial_read_pos)
serial_read_pos = serial_read_pos + 1
If serial_read_pos > 34 Then serial_read_pos = 0
If serial_byte = 0 Then
'this is the message termination byte
If serial_read_pos = serial_place_pos Then
'the last character received was this termination character
'so we've emptied the serial buffer
serial_has_data = 0
Else
'we've received more data following the termination character
'so once we've finished with this message, leave the has_data
'flag set and we'll parse the next lot of data
Endif
get_more_data = 0
Else
'read the data from the buffer, one byte at a time
Select Case serial_state
'--------------------------
Case serial_first_character
'--------------------------
'we've just had our first serial character: this should be the
'message type marker
If serial_byte = "M" Then
'this is a string message
serial_state = serial_lcd_msg
lcd_char_count = 0
Lcdcmdout LcdClear
Endif
If serial_byte = "P" Then
'this is positional data, sent as two lots of
'four character hex values (16-bit)
lcd_char_count = 0 'use this to count the serial bytes in
tmp_long = 0
is_moving = 1
serial_state = serial_get_position
Endif
If serial_byte = "V" Then
'this is the vacuum pen command, next byte
'will tell us whether to turn it on or off
serial_state = serial_vac_pen
Endif
If serial_byte = "S" Then
'this is the servo up/down command
serial_state = serial_servo
Endif
'------------------
Case serial_lcd_msg
'------------------
'this is a character to display on the LCD
Lcdout serial_byte
lcd_char_count = lcd_char_count + 1
If lcd_char_count = 16 Then Lcdcmdout LcdLine2Home
If lcd_char_count = 32 Then
Lcdcmdout LcdHome
lcd_char_count = 0
Endif
'-----------------------
Case serial_get_position
'-----------------------
If lcd_char_count < 4 Then
'this is another hex-based character for the x position
Gosub hex_to_value
tmp_long = ShiftLeft(tmp_long, 4)
tmp_long = tmp_long Or b_long
If lcd_char_count = 3 Then
target_x = tmp_long
tmp_long = 0
Endif
Endif
If lcd_char_count >= 4 And lcd_char_count < 8 Then
Gosub hex_to_value
tmp_long = ShiftLeft(tmp_long, 4)
tmp_long = tmp_long Or b_long
If lcd_char_count = 7 Then
target_y = tmp_long
'draw the received values to the LCD
Gosub draw_coords_target
Endif
Endif
lcd_char_count = lcd_char_count + 1
'------------------
Case serial_vac_pen
'------------------
If serial_byte = 49 Or serial_byte = 1 Then
'this is the "on" command (ascii value one)
High vac_pen_relay
Lcdcmdout LcdHome
Lcdout "Vac pen on "
Else
Low vac_pen_relay
Lcdcmdout LcdHome
Lcdout "Vac pen off "
Endif
'----------------
Case serial_servo
'----------------
If serial_byte = 49 Or serial_byte = 1 Then
'this is the "servo down" command (ascii value one)
servo_dir = 1
Lcdcmdout LcdHome
Lcdout "Pen down "
Else
'if you've not specifically put the pen down, lift it up
servo_dir = 0
Lcdcmdout LcdHome
Lcdout "Pen up "
Endif
Gosub set_servo
EndSelect
Endif
Wend
Endif
'as well as waiting for serial data, we can always respond to button presses
If jog_left = 0 And current_x > 0 Then
is_jogging = 1
motor_dir_x = 0
target_x = current_x - 1
Endif
If jog_right = 0 Then
is_jogging = 1
motor_dir_x = 1
target_x = current_x + 1
Endif
If jog_up = 0 And current_y > 0 Then
is_jogging = 1
motor_dir_y = 0
target_y = current_y - 1
Endif
If jog_down = 0 Then
is_jogging = 1
motor_dir_y = 1
target_y = current_y + 1
Endif
EndSelect
'------------------------------------------------------------------------
'if the motors need to be turned, move them in steps.
'This takes a bit of thinking about: we set our target x and y values
'as "number of HALF-steps" from current position. So if we want to target
'a point 5 steps away, we set the target_x to 10.
'Every half step, reduce the current_x so if we make a whole step, we
'need to reduce current_x by 2 (not by one)
'------------------------------------------------------------------------
If target_x < current_x Then
'we need to step closer to our target in the X axis
'provided the limit switch hasn't been hit
If limit_home_x = 0 Then
'we've hit the home limit switch, so set current and target x to zero
current_x = 0
target_x = 0
is_moving = 0
Hserout "LIMIT X HOME", CrLf
Lcdcmdout LcdHome
Lcdout " "
Lcdcmdout LcdHome
Lcdout "Home X reached "
Else
motor_dir_x = 0
step_difference_x = current_x - target_x
Gosub set_x_stepper_speed
Gosub move_x_stepper
current_x = current_x - 1
Endif
Endif
If target_x > current_x Then
'we need to step closer to our target in the X axis
If limit_extent_x = 0 Then
'k = 1
'If k = 2 Then
target_x = current_x
is_moving = 0
Hserout "LIMIT X EXTENT", CrLf
Lcdcmdout LcdHome
Lcdout " "
Lcdcmdout LcdHome
Lcdout "Limit X reached "
Else
motor_dir_x = 1
step_difference_x = target_x - current_x
Gosub set_x_stepper_speed
Gosub move_x_stepper
current_x = current_x + 1
Endif
Endif
If target_y < current_y Then
'we need to step closer to our target in the Y axis
'provided the limit switch hasn't been hit
If limit_home_y = 0 Then
'we've hit the home limit switch, so set current and target x to zero
current_y = 0
target_y = 0
Hserout "LIMIT Y HOME", CrLf
Lcdcmdout LcdHome
Lcdout " "
Lcdcmdout LcdHome
Lcdout "Home Y reached "
Else
motor_dir_y = 0
step_difference_y = current_y - target_y
Gosub set_y_stepper_speed
Gosub move_y_stepper
current_y = current_y - 1
Endif
Endif
If target_y > current_y Then
'we need to step closer to our target in the Y axis
If limit_extent_y = 0 Then
'k = 1
'If k = 2 Then
target_y = current_y
Hserout "LIMIT Y EXTENT", CrLf
Lcdcmdout LcdHome
Lcdout " "
Lcdcmdout LcdHome
Lcdout "Limit Y reached "
Else
motor_dir_y = 1
step_difference_y = target_y - current_y
Gosub set_y_stepper_speed
Gosub move_y_stepper
current_y = current_y + 1
Endif
Endif
'--------------------------------------------------------------
'this just helps stop the motors getting too hot so they're not
'always powered, when they are stationary
'--------------------------------------------------------------
If target_x = current_x And jog_left = 1 And jog_right = 1 Then Gosub stop_x_motor
If target_y = current_y And jog_up = 1 And jog_down = 1 Then Gosub stop_y_motor
'----------------------------------------------------------------
'if we've just stopped moving after reaching a target point, send
'a message back via serial to the host, to say we've arrived
'----------------------------------------------------------------
If is_moving = 1 Then
If target_x = current_x And target_y = current_y Then
is_moving = 0
Hserout "READY", CrLf
Gosub draw_coords_current
Endif
Endif
'-----------------------------------------------------------------------
'if we've just released one of the jog buttons, write the current XY out
'-----------------------------------------------------------------------
If is_jogging = 1 Then
If jog_left = 1 And jog_right = 1 And jog_up = 1 And jog_down = 1 Then
is_jogging = 0
Gosub draw_coords_current
Endif
Endif
Goto loop
End
On Low Interrupt
Resume
On High Interrupt
If PIR1.RCIF = 1 Then
'we've just received a byte from the serial port
High led_pin
serial_byte = RCREG
serial_buffer(serial_place_pos) = serial_byte
serial_place_pos = serial_place_pos + 1
If serial_place_pos > 34 Then serial_place_pos = 0
If serial_byte = 0 Then
'set the flag to say we've a full message in the serial buffer
serial_has_data = 1
Low led_pin
Endif
Endif
Resume
move_x_stepper:
High x_motor_enable
If motor_dir_x = 1 Then
x_motor_state = x_motor_state + 1
Else
If x_motor_state = 0 Then
x_motor_state = 3
Else
x_motor_state = x_motor_state - 1
Endif
Endif
If x_motor_state > 3 Then x_motor_state = 0
'once each step, the polarity of one coil should be reversed
Select Case x_motor_state
Case 0
Low x_coil_a_2
Low x_coil_b_2
High x_coil_b_1
High x_coil_a_1
Case 1
Low x_coil_a_1
Low x_coil_b_2
High x_coil_b_1
High x_coil_a_2
Case 2
Low x_coil_a_1
Low x_coil_b_1
High x_coil_b_2
High x_coil_a_2
Case 3
Low x_coil_a_2
Low x_coil_b_1
High x_coil_b_2
High x_coil_a_1
EndSelect
WaitMs step_delay_x
Return
stop_x_motor:
Low x_coil_a_1
Low x_coil_a_2
Low x_coil_b_1
Low x_coil_b_2
Low x_motor_enable
Return
move_y_stepper:
High y_motor_enable
If motor_dir_y = 1 Then
y_motor_state = y_motor_state + 1
Else
If y_motor_state = 0 Then
y_motor_state = 3
Else
y_motor_state = y_motor_state - 1
Endif
Endif
If y_motor_state > 3 Then y_motor_state = 0
'once each step, the polarity of one coil should be reversed
Select Case y_motor_state
Case 0
Low y_coil_a_2
Low y_coil_b_2
High y_coil_b_1
High y_coil_a_1
Case 1
Low y_coil_a_1
Low y_coil_b_2
High y_coil_b_1
High y_coil_a_2
Case 2
Low y_coil_a_1
Low y_coil_b_1
High y_coil_b_2
High y_coil_a_2
Case 3
Low y_coil_a_2
Low y_coil_b_1
High y_coil_b_2
High y_coil_a_1
EndSelect
WaitMs step_delay_y
Return
stop_y_motor:
Low y_coil_a_1
Low y_coil_a_2
Low y_coil_b_1
Low y_coil_b_2
Low y_motor_enable
Return
hex_to_value:
'ascii zero is 48, ascii A is 65
If serial_byte >= 65 Then
'if ascii A is 65, we want A=10, B=11 etc.
b_long = serial_byte - 55
Else
b_long = serial_byte - 48
Endif
Return
set_x_stepper_speed:
step_delay_x = 2
If step_difference_x < 20 Then step_delay_x = 8
If step_difference_x < 10 Then step_delay_x = 14
If step_difference_x < 4 Then step_delay_x = 25
If jog_left = 0 Or jog_right = 0 Then step_delay_x = 2
If jog_slow = 0 Then step_delay_x = 50
Return
set_y_stepper_speed:
step_delay_y = 2
If step_difference_y < 20 Then step_delay_y = 8
If step_difference_y < 10 Then step_delay_y = 14
If step_difference_y < 4 Then step_delay_y = 25
If jog_up = 0 Or jog_down = 0 Then step_delay_y = 2
If jog_slow = 0 Then step_delay_y = 50
Return
draw_coords_target:
Lcdcmdout LcdClear
Lcdout "Target position:"
Lcdcmdout LcdLine2Home
Lcdout "X:", #target_x, " "
Lcdout "Y:", #target_y
Return
draw_coords_current:
Lcdcmdout LcdClear
Lcdout "Current position"
Lcdcmdout LcdLine2Home
Lcdout "X:", #current_x, " "
Lcdout "Y:", #current_y
Return
set_servo:
'this is a quick and dirty blocking function
High led_pin
For j = 0 To 50
If servo_dir = 0 Then
ServoOut servo_pin, 150
Else
ServoOut servo_pin, 200
Endif
WaitMs 20
Next j
Low led_pin
Return
Our junk CNC controller has a few ways of driving it:
- Jog buttons to move the xy axis up/down/left/right
- Send serial data to give new destination co-ordinates; the CNC will move to the new target position as soon as possible
- Serial command to switch on a relay (which is connected to one side of the 240V mains supply of a vacuum pump operated pen, for picking up SMT components)
- Serial command to move a servo (used to lift the pen up or put it down)
Whenever the machine has received a new positional instruction and has completed the move to that location, a "ready" message is sent back to the host over serial, so that it knows the CNC head has arrived at its destination - this is preferable to using nasty timing loops to determine when the next command should be sent, from a long list of commands for the machine.
[video]
Unfortunately, even though it is highly accurate (to within 0.05mm) the flatbed scanner bed is running a little slow, even when the motor is spinning as quickly as possible (we have to have a 2ms delay between steps, otherwise the motor locks up). This is because of the multi-cog gearing before the belt. We tried driving the belt pulley directly from the motor, but it doesn't have enough torque - the gears not only slow the motor down, to allow for precise positioning while scanning, but also give it more torque than the motor can provide, if driving the belt directly.
So while, in principle, we're calling this a success (it can populate a pcb automatically from a list of co-ordinates) in practice it's actually quicker to place a dozen or so components by hand, using tweezers!