![DSC_0430[1] - コピー](https://kuriuzublog.files.wordpress.com/2017/06/dsc_04301-e382b3e38394e383bc.jpg?w=1100)

基板加工の流れ
- KiCADで基板を設計する
- KiCADからガーバーデータを出力する
- ガーバーデータをGynostemmaでiModela制御コマンドに変換する
- VirtualMODELAで結果を確認する
- iModela Controllerで変換したデータを使って切削
基板加工のための刃物
一番いいのは、オリジナルマインドで販売している土佐昌典VC。ただし、iModelaで土佐昌典VCを使うためには、シャンク径が違うためiModelaのコレットの改造が必要。
土佐昌典VCは本当にすばらしい。細くてシャープな線を切削してくれる。さすが基板専用カッターは違うと感動を覚えた。これは追加購入決定ですわ。
ここまでシャープだと表面実装もイケそう。すばらしい。
無改造iModelaで良い結果を得られたのは、0.3mmまたは0.4mmのボールエンドミル。土佐昌典VCと比較すると、折れやすい、切削速度が遅い、調整が難しい、細かいパターンが表現できない、バリが出やすい、寿命が短い、という問題がある。こういった点に気を付ければ使えるレベルではある。
iModelaのコレットの改造
iModelaのコレットの直径は標準だと2.35mmである。しかし、土佐昌典VCの直径は3.175mmである。つまり、直径が異なるので使うことができない。そこで、コレットを旋盤で加工し、穴を3.2mmに広げることで土佐昌典VCを使えるように改造した。
わざわざ加工なんぞしなくても直径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