iModelaで基板加工

DSC_0430[1] - コピー
iModela+土佐昌典VCによる基板加工
pcb0001
0.4mmボールエンドミルによる加工。穴あけは1.0mmドリル。

基板加工の流れ

  1. KiCADで基板を設計する
  2. KiCADからガーバーデータを出力する
  3. ガーバーデータをGynostemmaでiModela制御コマンドに変換する
  4. VirtualMODELAで結果を確認する
  5. iModela Controllerで変換したデータを使って切削

基板加工のための刃物

一番いいのは、オリジナルマインドで販売している土佐昌典VC。ただし、iModelaで土佐昌典VCを使うためには、シャンク径が違うためiModelaのコレットの改造が必要。
土佐昌典VCは本当にすばらしい。細くてシャープな線を切削してくれる。さすが基板専用カッターは違うと感動を覚えた。これは追加購入決定ですわ。
ここまでシャープだと表面実装もイケそう。すばらしい。

無改造iModelaで良い結果を得られたのは、0.3mmまたは0.4mmのボールエンドミル。土佐昌典VCと比較すると、折れやすい、切削速度が遅い、調整が難しい、細かいパターンが表現できない、バリが出やすい、寿命が短い、という問題がある。こういった点に気を付ければ使えるレベルではある。

iModelaのコレットの改造

iModelaのコレットの直径は標準だと2.35mmである。しかし、土佐昌典VCの直径は3.175mmである。つまり、直径が異なるので使うことができない。そこで、コレットを旋盤で加工し、穴を3.2mmに広げることで土佐昌典VCを使えるように改造した。

DSC_0427[1] - コピーDSC_0421[1] - コピー

わざわざ加工なんぞしなくても直径3.175mmのコレットを販売している会社はあったのだが、今はもう販売していないようだ。

Gynostemma RML-1 MOD

rerofumi氏が作成した「Gynostemma」という、ガーバーデータをGコードに変換するフリーソフトがある。今回は、GynostemmaをModelaシリーズの制御コマンド「RML-1」に対応させる改造を行った。

iModelaはわざわざRML-1に変換しなくてもGコードを直接読み込めるらしいのでこの改造はVirtual MODELAが使えるくらいしかメリットがない、かもしれない。

perf.iniとclass\gcode_formatter.rbを下記内容で上書きしてください。
perf.iniは土佐昌典VC用の設定になっています。

perf.ini

resolution 24
pass 1
cuttersize 0.25
patternfeed 0.5
patterndepth 0.06
outlinefeed 0.3
outlinelayer 0.4
boardthin 1.65
drilldepth 2.3
drilldwell 0
drillinputfix 4
threadnum 8

class\gcode_formatter.rb


require 'draw_primitive.rb'

# ------------------------------------------------------------
# --- utils
# ------------------------------------------------------------
module PathLineUtil
  def match_line(lineary, x, y)
    result = -1
    pp = PPoint.new
    pp.x = x
    pp.y = y
    pp.z = 0
    for i in 0...lineary.size
      item = lineary.at(i)
      p1 = PPoint.new
      p2 = PPoint.new
      p1.x = item['x1']
      p1.y = item['y1']
      p1.z = 0
      p2.x = item['x2']
      p2.y = item['y2']
      p2.z = 0
      if (p1 == pp) || (p2 == pp) then
        result = i
        break
      end
    end
    return result
  end

  def nearest_line(lineary, x, y)
    result = -1
    near = 100000000.0
    for i in 0...lineary.size
      item = lineary.at(i)
      vec1 = PPoint.new(item['x1']-x, item['y1']-y)
      if vec1.length < near then
        result = i
        near = vec1.length
      end
      vec2 = PPoint.new(item['x2']-x, item['y2']-y)
      if vec2.length < near then
        result = i
        near = vec2.length
      end
    end
    return result
  end

  def align_line(lineitem, x, y)
    match = false
    if ((lineitem['x1'] == x) && (lineitem['y1'] == y)) ||
        ((lineitem['x2'] == x) && (lineitem['y2'] == y)) then
      match = true
      if (lineitem['x2'] == x) && (lineitem['y2'] == y) then
        tx = lineitem['x1']
        ty = lineitem['y1']
        lineitem['x1'] = lineitem['x2']
        lineitem['y1'] = lineitem['y2']
        lineitem['x2'] = tx
        lineitem['y2'] = ty
      end
    else
      vec1 = PPoint.new(lineitem['x1']-x, lineitem['y1']-y)
      vec2 = PPoint.new(lineitem['x2']-x, lineitem['y2']-y)
      if vec2.length < vec1.length then
        tx = lineitem['x1']
        ty = lineitem['y1']
        lineitem['x1'] = lineitem['x2']
        lineitem['y1'] = lineitem['y2']
        lineitem['x2'] = tx
        lineitem['y2'] = ty
      end
    end
    return match
  end
end

# ------------------------------------------------------------
# --- GCode formatter for export
# ------------------------------------------------------------
class GCodeFormatter
  include PathLineUtil

  def initialize
    @linedata = []
    @seqdata = []
    @offset_x = 0.0
    @offset_y = 0.0
    @float_height = 2.0
    @layer_height = -0.4
    @mil_depth = -0.2
    @mil_feed = 0.2
    @spin_speed = 3600
    @position_x = 0.0
    @position_y = 0.0
    @position_z = 0.0
    @near_collect = 0.001
    @is_build = false
    @is_home = true
  end

  attr_accessor :offset_x
  attr_accessor :offset_y
  attr_accessor :float_height
  attr_accessor :mil_depth
  attr_accessor :mil_feed
  attr_accessor :spin_speed
  attr_accessor :layer_height
  attr_accessor :near_collect

  def clear
    @linedata.clear
    @seqdata.clear
    @offset_x = 0.0
    @offset_y = 0.0
    @float_height = 10.0
    @position_x = 0.0
    @position_y = 0.0
    @is_build = false
    @h_mirror = 1.0
  end

  def rebuild
    @is_build = false
  end

  def add_line(x1, y1, x2, y2)
    pos = {}
    pos['x1'] = x1
    pos['y1'] = y1
    pos['x2'] = x2
    pos['y2'] = y2
    @linedata << pos
    @is_build = false
  end

  def calc_offset
    @offset_x = 10000000.0
    @offset_y = 10000000.0
    @linedata.each do |item|
      if item['x1'] < @offset_x then
        @offset_x = item['x1']
      end
      if item['x2'] < @offset_x then
        @offset_x = item['x2']
      end
      if item['y1'] < @offset_y then
        @offset_y = item['y1']
      end
      if item['y2'] < @offset_y then
        @offset_y = item['y2']
      end
    end
  end

  def add_seq_move(lineitem)
    if ((lineitem['x1'] - @position_x).abs < @near_collect) &&
        ((lineitem['y1'] - @position_y).abs < @near_collect) &&
        (@is_home == false) then
      pos = {}
      pos['g'] = 'G01'
      pos['x'] = lineitem['x2']
      pos['y'] = lineitem['y2']
      pos['z'] = @position_z
      @seqdata << pos
    else
      pos = {}
      pos['g'] = 'G01'
      pos['x'] = @position_x
      pos['y'] = @position_y
      pos['z'] = @float_height
      @seqdata << pos
      pos = {}
      pos['g'] = 'G00'
      pos['x'] = lineitem['x1']
      pos['y'] = lineitem['y1']
      pos['z'] = @float_height
      @seqdata << pos
      pos = {}
      pos['g'] = 'G01'
      pos['x'] = lineitem['x1']
      pos['y'] = lineitem['y1']
      pos['z'] = @position_z
      @seqdata << pos
      pos = {}
      pos['g'] = 'G01'
      pos['x'] = lineitem['x2']
      pos['y'] = lineitem['y2']
      pos['z'] = @position_z
      @seqdata << pos
    end
    @position_x = lineitem['x2']
    @position_y = lineitem['y2']
    @is_home = false
  end

  def prepare_seq
    pos = {}
    pos['g'] = 'G00'
    pos['x'] = 0.0
    pos['y'] = 0.0
    pos['z'] = @float_height
    @seqdata << pos
    @is_home = true
  end

  def finish_seq
    pos = {}
    pos['g'] = 'G01'
    pos['x'] = @position_x
    pos['y'] = @position_y
    pos['z'] = @float_height
    @seqdata << pos
    pos = {}
    pos['g'] = 'G00'
    pos['x'] = 0.0
    pos['y'] = 0.0
    pos['z'] = @float_height
    @seqdata << pos
    @position_x = 0.0
    @position_y = 0.0
    @is_home = true
  end

  def build_path
    @seqdata.clear
    @position_z = 0.0
    prepare_seq
    while @position_z > @mil_depth do
      # -- depth
      if (@mil_depth - @position_z) < @layer_height then
        @position_z += @layer_height
      else
        @position_z = @mil_depth
      end
      # -- path
      workline = []
      @linedata.each do |item|
        citem = {}
        citem['x1'] = item['x1'] - @offset_x
        citem['y1'] = item['y1'] - @offset_y
        citem['x2'] = item['x2'] - @offset_x
        citem['y2'] = item['y2'] - @offset_y
        workline << citem
      end
      # make path
      while workline.size > 0 do
        index = match_line(workline, @position_x, @position_y)
        if index == -1 then
          index = nearest_line(workline, @position_x, @position_y)
        end
        lineitem = workline.at(index)
        workline.delete_at(index)
        align_line(lineitem, @position_x, @position_y)
        add_seq_move(lineitem)
      end
      finish_seq
    end
    @is_build = true
  end

  def save(filename)
    open(filename, "w") do |fp|
      fp << get_gcode_header
      #
      fp << get_path
      #
      fp << get_gcode_footer
    end
  end

  def get_path
    if @is_build == false then
      build_path
    end
    build_seq = ""
    @seqdata.each do |item|
      if item['g'] == "G00" then
          build_seq << sprintf("V4.0;\n")
      else
          build_seq <<  sprintf("V%.1f;",@mil_feed);
      end
      build_seq << sprintf("Z%d,%d,%d;\n",
                            item['x']*100, item['y']*100, item['z']*100)
    end
    return build_seq
  end

  def get_gcode_header
    gcode = ""
    gcode <<  sprintf(";;^IN;\n")
    gcode <<  sprintf("!MC1;\n")
    gcode <<  sprintf("V4.0;");
    gcode <<  sprintf("^PR;");
    gcode <<  sprintf("Z0,0,100;\n")
    gcode <<  sprintf("^PA;");
    return gcode
  end

  def get_gcode_footer
    gcode = ""
    gcode << sprintf("!VZ4.0;!ZM0;\n")
    gcode << sprintf("!MC0;^IN;\n")
    gcode << sprintf("\n")
    return gcode
  end

end

# ------------------------------------------------------------
# --- Drill
# ------------------------------------------------------------
class GCodeDrill < GCodeFormatter
  def initialize
    super
    @layer_height = -5.0
    @mil_depth = -2.0
    @mil_feed = 0.2
    @tool_set = []
    @tool_size = {}
    @position = []
    @scale = 100
    @dwell = 0
    @mil_size = 0.5
  end

  attr_reader :position
  attr_accessor :scale
  attr_accessor :dwell
  attr_accessor :mil_size
  attr_accessor :tool_set
  attr_accessor :tool_size

  def load(filename, h_mirror=1.0)
    drill_size = 0
    is_inch = false
    @seqdata.clear
    @position.clear
    open(filename, "r") do |fp|
      prepare_seq
      fp.each do |line|
        if line=~ /^INCH,/ then
          is_inch = true
        end
        if line=~ /^METRIC,/ then
          is_inch = false
        end
        if line=~ /^T([-0-9]+)/ then
          drill_size = $1.to_i
          if @tool_set.include?(drill_size) == false then
            @tool_set << drill_size
            @tool_size["#{drill_size}"] = 0.5
          end
        end
        # -- XY
        if line=~ /^X([-0-9]+)Y([-0-9]+)/ then
          a = $1
    	  b = $2
          @position_x = (a.to_f / @scale) * h_mirror
          @position_y = (b.to_f / @scale)
          if is_inch == true then
            @position_x *= 25.4
            @position_y *= 25.4
          end
          @position_x -= @offset_x
          @position_y -= @offset_y
          add_drill(drill_size)
        # -- X
        elsif line=~ /^X([-0-9]+)/ then
          a = $1
          @position_x = (a.to_f / @scale) * h_mirror
          if is_inch == true then
            @position_x *= 25.4
          end
          @position_x -= @offset_x
          add_drill(drill_size)
        # -- Y
        elsif line=~ /^Y([-0-9]+)/ then
    	 b = $1
          @position_y = (b.to_f / @scale)
          if is_inch == true then
            @position_y *= 25.4
          end
          @position_y -= @offset_y
          add_drill(drill_size)
        end
      end
      finish_seq
      @is_build = true
    end
  end

  def add_drill(drill_size)
    pos = {}
    pos['g'] = 'G00'
    pos['x'] = @position_x
    pos['y'] = @position_y
    pos['z'] = @float_height
    pos['tool'] = drill_size
    @seqdata << pos
    pos = {}
    pos['g'] = 'G01'
    pos['x'] = @position_x
    pos['y'] = @position_y
    pos['z'] = @mil_depth
    pos['tool'] = drill_size
    @seqdata << pos
    if @dwell > 0 then
      pos = {}
      pos['g'] = 'G04'
      pos['x'] = @position_x
      pos['y'] = @position_y
      pos['z'] = @mil_depth
      pos['msec'] = @dwell
      pos['tool'] = drill_size
      @seqdata << pos
    end
    pos = {}
    pos['g'] = 'G01'
    pos['x'] = @position_x
    pos['y'] = @position_y
    pos['z'] = @float_height
    pos['tool'] = 0
    @seqdata << pos
    pos = {}
    pos['g'] = 'G00'
    pos['x'] = @position_x
    pos['y'] = @position_y
    pos['z'] = @float_height
    pos['tool'] = drill_size
    @seqdata << pos
    pos = {}
    pos['x'] = @position_x
    pos['y'] = @position_y
    pos['tool'] = drill_size
    @position << pos
  end

  def save_drill(filename)
    @tool_set.each do |tsize|
      workseq = routing_tool(@seqdata, tsize)
      #
      dotsp = filename.split('.')
      last = dotsp.size
      if last > 1 then
        dotsp[last-2] = sprintf("%s_t%02d", dotsp[last-2], tsize)
      end
      outputname = dotsp.join('.')
      output(outputname, workseq)
    end
  end

  def output(filename, seqdata)
    open(filename, "w") do |fp|
      printf(fp, ";;^IN;\n")
      printf(fp, "!MC1;\n")
      printf(fp, "V4.0;^PR;\n")
      printf(fp, "V4.0;Z0,0,100;\n")
      printf(fp, "^PA;\n")
      #
      seqdata.each do |item|
        if item['g'] == 'G04' then
        elsif item['g'] == 'G00' then
          printf(fp, "V%.1f;\nZ%d,%d,%d;\n",
                 4.0,item['x']*100, item['y']*100, item['z']*100)
        else
          printf(fp, "V%.1f;\nZ%d,%d,%d;\n",
                 @mil_feed,item['x']*100, item['y']*100, item['z']*100)
        end
      end
      #
      printf(fp, "!VZ4.0;!ZM0;\n")
      printf(fp, "!MC0;^IN;\n")
      printf(fp, "\n")
    end
  end

  def routing_tool(allseq, tool)
    route = []
    workseq = []
    allseq.each do |point|
      if point['tool'] == tool then
        workseq << point
      end
    end
    current = PPoint.new(0.0, 0.0)
    while workseq.size > 0 do
      index = count = 0
      near = PPoint.new(workseq[0]['x'], workseq[0]['y']).length
      workseq.each do |nextseq|
        dist = PPoint.new(nextseq['x']-current.x, nextseq['y']-current.y).length
        if dist < near then
          index = count
          near = dist
        end
        count += 1
      end
      route << workseq.at(index)
      current.x = workseq[index]['x']
      current.y = workseq[index]['y']
      workseq.delete_at(index)
    end
    return route
  end

  def routing(workseq)
    route = []
    current = PPoint.new(0.0, 0.0)
    while workseq.size > 0 do
      index = count = 0
      near = PPoint.new(workseq[0]['x'], workseq[0]['y']).length
      workseq.each do |nextseq|
        dist = PPoint.new(nextseq['x']-current.x, nextseq['y']-current.y).length
        if dist < near then
          index = count
          near = dist
        end
        count += 1
      end
      route << workseq.at(index)
      current.x = workseq[index]['x']
      current.y = workseq[index]['y']
      workseq.delete_at(index)
    end
    return route
  end

end

# ------------------------------------------------------------
# --- One Opalation Drill
# ------------------------------------------------------------
class GCodeDrillOneOpelation < GCodeDrill
  def build_drill_path(round_resolution)
    @seqdata.clear
    @linedata.clear
    workpos = routing(@position)
    workpos.each do |item|
      size = (@tool_size["#{item['tool'].to_s}"] - @mil_size) / 2.0
      if size > 0 then
          add_drillpath(item['x'], item['y'], size, round_resolution)
      end
    end
    #
    @is_build = false
    build_path
  end

  def add_drillpath(x, y, size, res)
    p1 = PPoint.new(size, 0, 0)
    p2 = PPoint.new(size, 0, 0)
    rot = 360.0 / res.to_f
    p2.rotate_z(rot)
    for i in 0...res
      add_line(x+p1.x, y+p1.y, x+p2.x, y+p2.y)
      p1.rotate_z(rot)
      p2.rotate_z(rot)
    end
  end

end

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中