Showing posts with label cnc. Show all posts
Showing posts with label cnc. Show all posts

Thursday, 30 July 2015

Junk CNC controller from flatbed scanner and dvd rom drive - schematics and pcb layout

Earlier last week we put together a CNC from some left over junk. It went together surprisingly easily - since both the flatbed scanner and DVD rom drive had 4-wire (bipolar) stepper motors, all we really needed was to interface with an L293D h-bridge chip to get them moving.

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!)


We've already written a little bit about sourcing cheap stepper motors and how to investigate the type of motor and how to drive them, so there's nothing really new about that here. What is slightly different is the way we're handling incoming serial data.

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

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!

Tuesday, 7 July 2015

Building a pick-n-place machine from junk (flatbed scanner, dvd drive)

We've got some PCBs to solder up. A few hundred of the buggers. And, at the minute, they take aaaages. That's mostly because we haven't yet got a stencil for the solder paste, and the solder paste is quite old and not as fluid as it should be, so putting it down is like trying to glob jelly down with a pin - sometimes it sticks, sometimes not, sometimes it lifts off with the pin and so on. It's a nightmare!

But other than the solder paste application problem (which is to be solved now we've a nice stainless steel stencil on order) there's also the small issue of placing parts. Anyone who's ever done this with tweezers is probably familiar with the age old problems....


If you keep your 1206 resistors in nice, separated, pill boxes (or jewellers box compartments, or whatever you call them) there's a good chance that they don't all fall onto your desk the same way up! Picking resistors not only the right way up, but also the right way around (yes, we like all our numbers to be the same way around on a soldered-up board) can be a pain.

Then there's the issue of getting a dab of solder paste on the tip of your tweezers. Which acts like some kind of crazy kinetic-mind-melting glue, attracting all kinds of SMT components (and bits of fluff) from all over the desktop.

Once you've put the component down on the board, you have to remove the tweezers cleanly, without sneezing (or, more likely, twitching or shaking) as you remove them, unless you want to spend the next few minutes nudging the component back into it's desired position. Nudge a bit, no, too much, back a bit, no, back where it was.....

We've been saying for some time we need to make a pick-n-place machine.
The only thing is, they're pretty time consuming. What we really need is a quick hacked-up pick-n-place machine, made from junk that's just lying around, and can be quickly thrown together and made to work. Since our PCBs are only very narrow (100mm x just 14mm wide) there's even a case for making a one-time pick-n-place, just for this one job. After all, if it can be done quickly, and cheaply, then there's no harm in it being a single-job machine, rather than a generic device capable of handling lots of different boards (which it would need to be if we were going to spend any great money on it!)

It just so happened that we had an old flatbed scanner lying around, and a knackered DVD rom drive. Perfect for making a simple xy carriage!

Unusually our flatbed scanner had just one rail, straight down the centre, rather than a rail either side as we'd expected. No matter, this will just have to do for now.

The flatbed scanner was easily dismantled. At first we were just going to take the stepper and the belt from it, but then it occurred to us that the actual plastic frame may just make a good enough y axis for our pick-n-place - after all, it was a good enough y-axis for a scanner, taking in images at 1200dpi. It has a footprint of just over A4 (so quite small) but even if we only used one "half" of the bed (splitting it long-ways and adding a second rail along one side) there's still plenty of travel in the y-direction and enough width for the x-carriage made from a DVD rom drive.


After putting back the belt and carriage system, we very quickly had a working y-axis. The x-carriage would mount on the black plastic piece that the current belt system drives up and down along the 6mm stainless steel rod. The y-axis is actually quite nicely assembled, for such a cheap scanner.

The original scanning head was pulled along by a single 48 steps/rev (7.5 degree) stepper motor.


The underside looked very interesting - it's been geared down quite a bit. So this will be great for super-precise positioning. Our only concern is that it doesn't slow things down too much!


The linear rail is simply a polished stainless steel rod with a piece of plastic clipped over it. The toothed belt sits in part of the plastic that has been moulded to fit the teeth - no need for clamping here! The rod feels like it may have had silicon grease applied at some point in the past. It's not the best quality for a carriage assembly, but it moves smoothly (no binding) and there's no wiggle in it as it travels up and down - everything you could want from a linear rail system!


The tension is maintained in the belt by a simple spring-loaded, free spinning pulley on the far end (opposite the motor). The pulley is not toothed, so the belt just passes around it. The spring ensures that the belt is under tension, without putting it under too much strain.


For the x-axis we butchered a old, broken, DVD rom drive.
It doesn't have particularly fine resolution, but should be just about good enough for what we're needing; after all, our PCBs can withstand placement up to 0.5mm or more out-of-place. If we're going to place all our components and use either hot air or an over to "bake" the boards, any slight mis-alignment will be corrected as the solder paste melts anyway.


To get at the carriage, simply keep removing "layers" from the DVD rom drive (retaining any parts that might look useful along the way) until the optical head positioning assembly is accessible.


This part had actually been glued (presumably with some kind of epoxy) to the cast aluminium frame, so required a bit of "persuasion" to release it. Much to the relief of the other nerds, the hammers stayed locked away for this (though a selection flat-ended screwdrivers made for great levers to prise apart the different layers)


We kept the ribbon cable from the little stepper motor intact, but it's quite trivial to solder a couple of wires to the connection points on the top, if it should get damaged. We took off as much of the optical reader hardware as possible from our carriage, until we were left with very little

The optical reader assembly had some interesting bits, including some tiny little mirrors and this tiny - but functional - magnifying lens!

With the two assemblies dismantled, it was now a matter of trying to get the stepper motors to work independently, before we put everything back together and try running it as an actual xy cnc.
This meant putting together a test rig, with a PIC microcontroller (what else?) some L293D H-bridge IC chips (thanks for those Jason) and - just for fun - a 16x2 character LCD to report what was happening (and when the thing is finished, to display which component is being placed, and where)



Sure, it just looks like a tangle of wires (though Steve should be impressed that there are plenty of different colours, to distinguish between each part of the circuit, instead of our usual trick of wiring everything in green wire, then not knowing where to start when it comes to debugging!). Sure, it's just a microcontroller driving a couple of steppers (no Arduino copy-n-paste library code here though!). But it does demonstrate that we've got the basic control over an x and a y axis, both using jog buttons, and by sending "destination co-ordinates" over serial.

Originally it was tempting to just throw an Arduino at the h-bridge chips and get the motors spinning, but in the end we went with the big beefy 18F4550 PIC for a couple of reasons:
  • Arduino libraries are of variable quality. Most are ok. Some are quite clever. A few are shockingly poor. If there are any problems trying to run multiple things together (like receiving serial while maintaining two steppers, and servicing a servo, as well as responding to manual jog controls) eliminating the libraries is the first place to start - so why use them in the first place?
  • We're going to throw quite a bit of stuff at this, and also quite liked the idea of a character LCD to report back which component the device is picking, what it's target destination/co-ordinates are etc. This means we need quite a few pins.
  • Manual jog buttons (up/down/left/right plus a "jog-slowly" option) means lots of I/O pins are needed. Probably more than we could get from a '328 AVR without resorting to shift registers or IO expanders.

Even with a massive 40-pin chip, we used up a lot of IO lines:


A full explanation of the code and ideas behind it (including interrupt-driven, circular buffered serial receive) will follow in a later post. For now, here's a video of some very crude testing:



Although you might blink and miss it, there's also a simple ramp-down function as the carriage starts to approach it's destination point - you can see it in the first motor, as it slows to a stop, a hear a definite change in tone of the motor as the DVD carriage reaches the destination point.

Friday, 21 June 2013

CNC testing with Mach3

It has taken a bit of trial and error and a good, old-fashioned, low-tech method of validating our CNC setup, but we're thrilled to see that it finally looks like it's working.

To begin with, we placed a pen (instead of a drill) in the z-axis and fiddled with the motor tuning settings in Mach3. We had no idea how many turns per unit (i.e. millimetre) we needed, so went with a default of 200 (assuming that the steppers were 200 steps/rev). The grid it plotted looked like we'd used inches instead of millimetres - the gaps between the dots were massive!

To simplify things, we replaced our grid with a row of three dots, using the g-code

G0 Z0
G0 X0 Y0
G0 Z2
G0 Z0
G0 X10 Y0
G0 Z2
G0 Z0
G0 X20 Y0
G0 Z2
G0 Z0


to draw a row of three dots on a sheet of paper. We checked our Mach3 settings and confirmed that we had, indeed, set the default units to mm and not inches. After measuring with a steel rule, we confirmed that the dots were not actually an inch apart either.

So we were pretty sure that the machine was set up for millimetres (but not 100% convinced) but had no idea of the leadscrew pitch. Inspired by Steve's coding method - where if something works but not quite, you keep applying multipliers and choosing different numbers until it's close enough - we set to work changing the values in the Mach3 settings. Firstly by reducing the steps per millimetre down to 100.

This time, the spacing between the dots looked better, but we were now about 12mm apart, not 10.
Reducing the steps per millimetre to 90 brought the dots too close together, but it seems that 95 is the magic number for our machine:


Each dot was perfectly 10mm apart - exactly as the g-code described.


(the pattern of dots in this photo shows a grid supposedly drawn at 0.1" pitch, but at 100 steps per mm, our machine drew them slightly too far apart. Repeating this later, at 95 steps per mm, resulted in a perfect 0.1" grid)

Thursday, 20 June 2013

Parsing Inkscape SVG files to create g-code

Here's a screenshot of our first test - and it's all looking rather encouraging!


I actually got the generate g-code (after parsing the SVG file and applying modifiers) to match the positioning of the objects in Inkscape - in the example above, you can see that the x co-ordinate is the same in both the VB app and in Inkscape.

The y value is different but only because Inkscape insists on putting the origin (0,0) at the bottom left hand corner of the document, whereas most of us (and most other apps) expect it to be top-left. In fact, when I consider that my svg image is 99.5mm high, the numbers do stack up.

In my g-code I had the y axis value as 10.41 whereas Inkscape insisted it was 89.11.
However, subtract my 10.41 from 99.5 and you get....erm.... 89.01
So it looks like my outline is actually 99.52mm (so I was 0.02mm out!). I think this kind of tolerance is acceptable for a machine that has a 0.1mm accuracy.

All that remains now is to work out how many turns on our CNC we need to make to travel 1mm and get all that coded into the Mach3 interface. With a bit of luck we'll be able to drill some test boards (and hopefully actually print some PCBs) very, very soon!

Wednesday, 19 June 2013

Making NC drill files and GCode from a PDF (via Inkscape)

Mach3 is a serious bit of kit. There are options and settings for everything!
Which a lot of people would be really pleased with. Just like Eagle is the de-rigueur PCB layout software, because it's so configurable. But here at Nerd Towers, we're not really fans of spending ages getting things set up and working (hence the love of Windows over Linux, and ExpressPCB over Eagle) so - bizarrely - we'll often spend ages making our own tools, in order to "just get the job done". It's not always quicker, and it's most often less simple. But it does give us a certain degree of control over what we're doing.

Which is why we're struggling so much with Mach3 - and in particular importing dxf files.
Mach3 is simple when it comes to "load some g-code and run it". We can even make our own simple g-code files, load them and run them in Mach3. But getting the drill data out of our ExpressPCB files is pretty tricky.

We've already successfully created a .dxf file from ExpressPCB, via Inkscrape, but now, getting the drilling data into Mach3 is proving difficult: so we're writing our own Inkscape-to-gcode app to get us up and running. After all, the pdf loads easily enough into Inkscape, and saving as .svg gives us a whole load of co-ordinates in a nice, easy to read/parse, xml-formatted file.

Here's our PCB layout, loaded from a PDF (which in turn was generated by printing via CutePDF from ExpressPCB). We've picked out the drill holes and coloured them red, the board outline and coloured it gree and ditched everything else. We used a technique discussed earlier to do this.


Saving as plain svg and opening the file in Notepad/Editplus (or any other text editor) gives us our xml which we can parse away at leisure.

One thing to note is that all the co-ordinates listed in the file are in pixels (not mm) as per the .svg specification. It's worth noting that Inkscrape, by default, exports at 90dpi. Knowing this, we can multiply all co-ordinates in the file by 25.4/90 to get the distances in millimetres. (there are 25.4 millimetres in an inch, and the co-ords are saved in a file, where 90 pixels = 1 inch, so 90 pixels = 25.4 millimetres)

Also, Inkscape makes use of translations quite a lot. In this case, the image was drawn in a particular area of the screen. After we got rid of all the extra data that wasn't needed, the result was placed in the bottom left hand corner of the document. It seems that rather than update the points positions, Inkscape simply applies a master translate command over the whole of the image data.
This needs to be kept in mind when parsing the plotting points in the xml file


Where are point is listed as, for example, (390.044, 735.075) it's actually placed at (390.44, 39.06) since the entire image has a translation of [0,-698.16927] applied to it and 735.07543-698.16927=36.90616)

The reason for choosing green and red in our image file is to make identifying the parts easier in the xml. Anything that has a fill colour #ff0000 is red - and therefore a drill point. Likewise, anything with a fill colour of #00ff00 is green - and makes up the board outline.

All of our holes are described as a series of points describing a circle. We're only really interested in the initial (start) point of each hole, not all the junk that comes after it:


With all this in mind, it should be a simple job to knock up some VB to parse an SVG file and create the necessary move-and-peck-and-drill g-code (move to point, z-down, z-up, move to point, z-down, z-up etc). Updates coming soon....

Manufacturing PCBs at home

A while back we started looking at the feasibility of manufacturing at home. The reasons are many, but that's a discussion for another day. So far, we've been looking at how to screen print double-sided pcbs before immersing in ferric chloride for etching.

Now double-sided boards mean finding a way of lining up the prints on both sides of a board. And the most obvious way to do this is to pre-drill the boards before printing. Which in turn means using a CNC machine.

We built a CNC drilling machine for under £50 last year which was pretty accurate - up to about 0.5mm which still made for some pretty useable PCBs. One idea (which we're not dismissing - just haven't time to implement just yet) is to add a camera to the drill head and drill a pre-printed (but not-yet-etched) board. The drill head can move into position, then we use the image from the camera to ensure the drill goes exactly into the centre of each hole by jogging the x/y axis a little until perfectly lined up. This should at least remove the 0.5mm inaccuracies we got during testing. With one side printed and the board drilled, we could then line up the board and print the second side before etching the entire thing.

But just right at this minute, we've not really got time to do more development - especially not when we've a 0.1mm accurate CNC milling machine gathering dust under a table somewhere....

It's been a few years since this thing was fired up, and it was supplied with RoutoutCNC software (incidentally, this is now being given away with free licences since it's no longer supported) but we couldn't get it to work properly. Each time the x-axis moved, the z-axis travelled downwards about once every 10 steps.

Mach3, however, worked just fine (once we'd spent a few hours setting it up and replacing the 25-pin serial cable we thought was a parallel one with a proper, straight-through parallel cable). Here's the CNC in jog mode.


The CNC machine has been made for milling. It's a sturdy, chunky beast and uses really good quality lead screws on each axis. So it's pretty accurate and has very good repeatability. The only thing is - it's so sloooowww.

It'll have to do for now. Maybe hacking our own mini drill machine in future isn't such a bad idea after all!

RoutoutCNC Free Licence Keys:
Viewer: M72DXDU-8X5TQ6L-KPVADQT-9EK0DUD
Manager: L4CD9XT-DK5P5PK-TKHA2AK-D7WRCGC