動機
パラレルポートとかシリアルポートを介したライターを使っていた古(いにしえ)の時代からインターフェース戦国時代を経てUSBが天下統一を果たすもマイコンライター界は未だAVRでAVRを焼くPICでPICを焼くという鶏卵(ライターが先か石が先か)問題の解決を見いだせないまま(僕はPICkit2が壊れた時のためバックアップ用として秋月のAE-18F2550にPICkit2ファームを入れて封印し)現代に至っています。安価でUSB-DFU(Device Firmware Upgrade)が使えるCH55xでUSBaspを実装して下さった神が現れAVR界に光が差しましたがPIC界は混沌としたままです。このままHVPが歴史の彼方に忘れ去られれば良かったのかもしれませんが秋月の特売でPIC16F747を一生分衝動買いしてしまったので、、、w
CH55xは工場出荷の段階でDFUできるファームウエアが焼き込まれているので生石のまま自分自身にコードを焼けます。なのでCH55xがUSBaspをエミュレートできれば鶏卵問題(最初のAVRはどうするのか)が解決する理屈と同じでWriter509(でもPICkit2でも何等かのPICライターに対応したハードやソフト)をエミュレートしちゃえば解決なんです。でもググってもチャッピーに聞いてもナイスなモノ(CH55xで動くファームウエア)を見つけられませんでした。仕方が無いんで言い出しっぺの法則に基づいてPIC16F747だけでも焼けるライターでも作ってみる事にしました。昨日、試しに作ったarduino uno用のプロトタイプPIC-Writerをch552pに移植してみます。ChatGPTに手伝ってもらても結局、複雑な条件になるとダメなんで肝心なところは自分で書くハメになります。CH55xのIO方向切り替えでハイインピーダンス問題にハマるのは御約束なんでちゃんと地雷を踏みました。信じる者は足元をすくわれるってヤツですね。
配線
純粋にCH552Pの事だけを考えれば良いようにArduino UNO用に昨日作った動作確認済み回路をそのまま流用します。VPPが実測で12.64Vのママなんで気持ち悪い人はチャンとした回路を作った方がいいです。

| CH552P | PIC16F747 | ICSP役割 |
|---|---|---|
| P1.4(Pin 3) | PGD (Pin 40) | データ(双方向) |
| P1.5(Pin 2) | PGC (Pin 39) | クロック(Writer→PIC) |
| P1.6(Pin 5) | VPP (Pin 1) | VPP電圧制御(12.6V) |
| P1.7(Pin 6) | VDD (Pin 20) | VDD制御(5.0V) |
CH552P用のPIC-Writerプログラム
/*
PIC16F747 Single-purpose ICSP writer
Yet Another PIc wriTER
Author: 2ru
*/
#include <Arduino.h>
#include "userUsbCdc/USBCDC.h"
#define VDD_IS_NEGATIVE_LOGIC 1 // // 2SA1015を1石でハイサイド負論理スイッチしている
#define debug 0
// Hardware layer
// =====================
// ピン定義
// =====================
// CH552P → PIC16F747 ICSP役割
#define PGD_BIT 4 // P1.4(Pin 3) PGD (Pin 40) データ(双方向)
#define PGC_BIT 5 // P1.5(Pin 2) PGC (Pin 39) クロック(Writer→PIC)
#define VPP_BIT 6 // P1.6(Pin 5) VPP (Pin 1) VPP電圧制御(12.6V)
#define VDD_BIT 7 // P1.7(Pin 6) VDD (Pin 20) VDD制御(5.0V)
#define PGD_MASK (1<<PGD_BIT)
#define PGC_MASK (1<<PGC_BIT)
#define VPP_MASK (1<<VPP_BIT)
#define VDD_MASK (1<<VDD_BIT)
#define PORT P1
// =====================
// 単純に各PINをオンオフするprimitve layer
// =====================
inline void PGC_H(){PORT|= PGC_MASK;}
inline void PGC_L(){PORT&=~PGC_MASK;}
inline void PGD_H(){PORT|= PGD_MASK;}
inline void PGD_L(){PORT&=~PGD_MASK;}
inline void VPP_H(){PORT|= VPP_MASK;} // VPPは2SA1015+2SC1815の2石で
inline void VPP_L(){PORT&=~VPP_MASK;} // High Voltegeを正論理制御
#if VDD_IS_NEGATIVE_LOGIC
inline void VDD_L(){PORT|= VDD_MASK;} // VDD制御を負論理ならHが0でLが1になる
inline void VDD_H(){PORT&=~VDD_MASK;} // 制御せずVDDを5Vに直結という力業もアリ
#else
inline void VDD_H(){PORT|= VDD_MASK;} // VDD制御を正論理に
inline void VDD_L(){PORT&=~VDD_MASK;} // する場合はコッチにする
#endif
// 電源投入直後のPIN初期化
inline void InitPINmode() {
// 各機能ピンを出力にセットする
P1_MOD_OC &= ~(PGD_MASK|PGC_MASK|VPP_MASK|VDD_MASK); // 0000xxxx → Push-Pull
P1_DIR_PU |= (PGD_MASK|PGC_MASK|VPP_MASK|VDD_MASK); // 1111 0000 → 出力
VPP_L();
VDD_L();
PGC_L();
PGD_L();
}
// bit banging. physical layer
void writeBit(byte b) { // Writer->PICに1bit送信
if(b)
PGD_H();
else
PGD_L();
PGC_H();
PGC_L();
}
byte readBit() { // PIC->Writerに1bit受信
PGC_H();
byte r=(P1 & PGD_MASK)?1:0;
PGC_L();
return r;
}
// PGD-pin-の入力出力を切り替えるルーチン。インラインの必要はないかな
inline void readPINmode() { // PGDピンを入力に切り替える
P1_DIR_PU &= ~PGD_MASK; // まず入力(=0)に切り替える
PGD_H(); // 自分でHIにしつつ(プルアップ)
P1_MOD_OC |= PGD_MASK; // 入力時は必ずOpenDrain(実質Hi-Z)にする
}
inline void writePINmode() { // PGDピンを出力に切り替える
P1_MOD_OC &= ~PGD_MASK; // 毎度 0 (=Push-Pullモード)にする
P1_DIR_PU |= PGD_MASK; // 出力(=1)に切り替える
}
/////////////// ここまでがprimitve layer
/////////////// 以下は移植しても書き換える必要がない事を希望
// Transmit ICSP commands via bit-banged physical layer signals.
// bit banging physical layerを使ってPICとICSPコマンドで通信する
uint16_t pc;
void ICSPcmd(byte cmd) {
noInterrupts();
for(byte i=0;i<6;i++) {
writeBit(cmd&1);
cmd>>=1;
}
PGD_L();
interrupts();
}
// 14bitのパラメータをPICに送信
void ICSPwrite(word d) {
noInterrupts();
writeBit(0);
for(byte i=0;i<14;i++) {
writeBit(d&1);
d>>=1;
}
writeBit(0);
PGD_L();
interrupts();
}
// PICから14bitのデーターを受信
word ICSPread() {
word data=0;
readPINmode();
noInterrupts();
readBit();
for(byte i=0;i<14;i++)
if(readBit())
data|=1<<i;
readBit();
writePINmode();
PGD_L();
interrupts();
return data;
}
enum ProgrammingModeSequence {
VDD1st,
VPP1st
};
// HVPモード開始
void enterProgrammingMode(byte flag) {
PGD_L(); PGC_L();
switch(flag) {
case VPP1st:
VPP_H(); // VPPを先に立ち上げる
delay(10);
VDD_H();
break;
case VDD1st:
VDD_H(); // VDDを先に立ち上げる
delay(10);
VPP_H();
}
pc=0; // exitProgrammingMode()でpc=0してれば無くてもいいはず
}
// HVPモード終了
void exitProgrammingMode() {
PGD_L();
PGC_L();
VPP_L(); // 作法としてVPPを先にoffの方が無難
VDD_L();
pc=0;
}
/////////////// Host-PCとライター間の interface rayer
/////////////// ポーティング毎に書き換える必要がある
#define NextDataReady() USBSerial_available()
#define readByte() USBSerial_read()
#define writeByte(b) USBSerial_write(b)
// 上記interface 3関数を使ってワード送受信のラップレイヤー
void writeWord(word w) {
writeByte((byte)(w & 0xFF)); // 下位バイトを先に書き出す
writeByte((byte)((w >> 8) & 0xFF)); // 上位バイト
}
word readWord() {
byte lo, hi;
while(!NextDataReady()); // pcからの入力を待つ
lo=readByte(); // 先に来たバイトは下位
while(!NextDataReady());
hi=readByte();
return (word)lo | ((word)hi << 8);
}
// ===== ICSP コマンド =====
#define LoadConfiguration 0b000000
#define LoadDataforProgramMemory 0b000010
#define ReadDatafromProgramMemory 0b000100
#define IncrementAddress 0b000110
#define BeginProgrammingExternally 0b011000
#define EndProgramming 0b011110
#define BulkEraseProgramMemory 0b001001
#define BulkEraseDataMemory 0b001011
static word tprog; // 書き込み終了を待つ時間。μ秒単位
// Writerインタープリタからディスパッチされる各機能のhandler レイヤー
// 書き込み番地をadrsに設定する
void goAdress(word adrs) {
if(0x2000<=adrs) {
ICSPcmd(LoadConfiguration); ICSPwrite(0x0000);
ICSPcmd(LoadDataforProgramMemory); ICSPwrite(0x3FFF);
pc=0x2000;
} else if(adrs<pc) { // Program memory 巻き戻す必要がある
VPP_L(); // VPPだけ入れなおす。pc=0に戻される
delay(10);
VPP_H(); // HVPに再入
pc=0;
}
delay(10); // 安定するまで待ってから
while(pc<adrs) { // ConfigでもProgram memoryでadrsまでpcを進める
ICSPcmd(IncrementAddress);
pc++;
}
writeWord(pc); // エコーバック
}
// チップイレース
void ICSP_BulkErase() {
ICSPcmd(LoadConfiguration); ICSPwrite(0x0000);
ICSPcmd(LoadDataforProgramMemory); ICSPwrite(0x3FFF);
ICSPcmd(BulkEraseProgramMemory);
ICSPcmd(BulkEraseDataMemory);
delay(10);
ICSPcmd(EndProgramming);
}
// 現在のpc番地からlenワード長を読みだしてホストに転送
void pic2host(int len) {
for(int i=0;i<len;i++) {
ICSPcmd(ReadDatafromProgramMemory);
writeWord(ICSPread());
ICSPcmd(IncrementAddress); pc++;
}
}
#define RowSize 2
word buf0,buf1;
void host2pic(int len) {
int f,w;
if(len==0)
return;
if(pc&1) { // 奇数番地始まりなら1ワード書き込みをする
// 書き込み開始は必ず偶数番地になるよう1ワードフェッチして書き込み
buf0=readWord();
ICSPcmd(LoadDataforProgramMemory); ICSPwrite(buf0); w++;
ICSPcmd(BeginProgrammingExternally);
delayMicroseconds(tprog);
ICSPcmd(EndProgramming);
ICSPcmd(IncrementAddress); pc++;
len--;
}
if(RowSize<=len) {
buf0=readWord();
buf1=readWord();
f=2;// 次にフェッチする番地=フェッチ済みワード数
} else { // if(1==len)
buf0=readWord();
f=1;
}
w=0;
// 最終ブロックの読み込みを完了し書き込む前に終了
while(w+RowSize<len) {
ICSPcmd(LoadDataforProgramMemory); ICSPwrite(buf0); w++;
ICSPcmd(IncrementAddress); pc++;
ICSPcmd(LoadDataforProgramMemory); ICSPwrite(buf1); w++;
ICSPcmd(BeginProgrammingExternally);
buf0=readWord(); f++;
if(f<len) {
buf1=readWord(); f++;
}
delayMicroseconds(tprog);
ICSPcmd(EndProgramming);
ICSPcmd(IncrementAddress); pc++;
}
// 最後のブロックは書き込みだけで読み込まない
ICSPcmd(LoadDataforProgramMemory); ICSPwrite(buf0); w++;
if(w<len) {
ICSPcmd(IncrementAddress); pc++;
ICSPcmd(LoadDataforProgramMemory);
if(f>w) {
ICSPwrite(buf1);
} else {
ICSPwrite(0x111);
}
w++;
}
// 残りの端数ワードを焼く
ICSPcmd(BeginProgrammingExternally);
delayMicroseconds(tprog);
ICSPcmd(EndProgramming);
}
#if debug
#define BufSiz 16
word data[BufSiz];
void maketestdata(word *buf, int len) {
for(int i=0;i<len;i++) {
buf[i]=pc+i;
}
}
void buf2pic(word *buf,int len) {
for(int i=0;i<len;i++) {
ICSPcmd(LoadDataforProgramMemory); ICSPwrite(buf[i]);
if(pc & 1) {
// 奇数アドレス → 書き込み実行
ICSPcmd(BeginProgrammingExternally);
delayMicroseconds(tprog);
ICSPcmd(EndProgramming);
}
ICSPcmd(IncrementAddress); pc++;
}
// 最後が偶数で終わった場合のフラッシュ
if(pc & 1) {
ICSPcmd(BeginProgrammingExternally);
delayMicroseconds(tprog);
ICSPcmd(EndProgramming);
}
}
void pic2buf(word *buf,int len) {
for(int i=0;i<len;i++) {
ICSPcmd(ReadDatafromProgramMemory);
buf[i]= ICSPread();
ICSPcmd(IncrementAddress); pc++;
}
}
void Host2Buf(word *buf,int len) {
for(int i=0;i<len;i++){
buf[i]=readWord();
}
}
void buf2host(word *buf,int len) {
for(int i=0;i<len;i++){
writeWord(buf[i]);
}
}
word readHexWord() {
word v=0;
for(int i=0;i<5;i++) {
if(NextDataReady()) {
char c = readByte();
if(('0'<=c)&&(c<='9'))
v=v*16+c-'0';
else if(('A'<=c)&&(c<='F'))
v=v*16+c-'A'+10;
else if(('a'<=c)&&(c<='f'))
v=v*16+c-'a'+10;
else
break;
}
}
return v;
}
// ===== デバック用HEXダンプ =====
const char asc[] = "0123456789ABCDEF";
void write_hex_byte(byte v) {
writeByte(asc[(v >> 4) & 0x0F]);
writeByte(asc[v & 0x0F]);
}
void write_hex_word(word w) {
write_hex_byte((w >> 8) & 0xFF);
write_hex_byte(w & 0xFF);
}
void dumpBuf(word *buf,int len) {
for(int i=0;i<len;i++) {
if(((pc-len+i)%16)==0) {
writeByte('\r');
writeByte('\n');
write_hex_word(pc-len);
writeByte(':');
}
writeByte(' ');
write_hex_word(buf[i]);
}
}
#endif
// Application layer
void interpreter() {
byte c;
#if debug
word len;
#endif
if(!NextDataReady())
return;
switch(c=readByte()) {
case '{': enterProgrammingMode(VDD1st); break;
case '}': exitProgrammingMode(); break;
case 'E': ICSP_BulkErase(); break;
case 'R': pic2host(readWord()); break;
case 'W': host2pic(readWord()); break;
case 'G': goAdress((int)readWord()); break;
case 'T': tprog=readWord(); break;
#if debug
case '\r':
case '\n':
case ' ':
return;
case '(': VDD_H(); break;
case ')': VDD_L(); break;
case '[': VPP_H(); break;
case ']': VPP_L(); break;
case 'w':
goAdress(readHexWord());
len=readHexWord();
while(BufSiz<=len) {
maketestdata(data,BufSiz);
buf2pic(data,BufSiz);
len-=BufSiz;
}
if(0<len) {
maketestdata(data,len);
buf2pic(data,len);
}
break;
case 'r':
goAdress(readHexWord());
len=readHexWord();
while(BufSiz<=len) {
pic2buf(data,BufSiz);
dumpBuf(data,BufSiz);
len-=BufSiz;
}
if(0<len) {
pic2buf(data,len);
dumpBuf(data,len);
}
break;
case 'v':
USBSerial_print("Yat Another PIc-wriTER\n");
break;
default:
// 未定義コマンド
writeByte('N');
return;
#endif
// writeByte(c); // 正常に終わった場合はコマンドをエコーバックする
}
}
// ===== Arduino Setup =====
void setup() {
InitPINmode();
tprog=300;
exitProgrammingMode();
// Serial.begin(115200);
}
void loop() {
interpreter();
}
PC側のホストプログラム
import serial
import time
import argparse
import os
from datetime import datetime
import sys
# =========================
# HEX → セグメント
# =========================
def parse_hex_segments(filename):
mem = {}
with open(filename) as f:
base = 0
for line in f:
if not line.startswith(':'):
continue
length = int(line[1:3], 16)
addr = int(line[3:7], 16)
rectype= int(line[7:9], 16)
data = bytes.fromhex(line[9:9+length*2])
if rectype == 0x00:
full = base + addr
for i in range(0, length, 2):
w = data[i] | (data[i+1] << 8)
a = (full + i) // 2
mem[a] = w
elif rectype == 0x04:
base = int.from_bytes(data, 'big') << 16
return dict_to_segments(mem)
def dict_to_segments(mem):
if not mem:
return [], {}
segs = []
keys = sorted(mem.keys())
start = keys[0]
cur = [mem[start]]
prev = start
for k in keys[1:]:
if k == prev + 1:
cur.append(mem[k])
else:
segs.append((start, cur))
start = k
cur = [mem[k]]
prev = k
segs.append((start, cur))
return segs, mem
# =========================
# PIC Writer
# =========================
class PICWriter:
def __init__(self, port, baud=115200):
self.ser = serial.Serial(port, baud, timeout=1, write_timeout=1)
time.sleep(2)
self.ser.reset_input_buffer()
def send(self, s):
if isinstance(s, str):
s = s.encode()
self.ser.write(s)
def write_word(self, w):
self.ser.write(bytes([w & 0xFF, (w >> 8) & 0xFF]))
def read_word(self):
b = self.ser.read(2)
if len(b) < 2:
raise IOError("timeout")
return b[0] | (b[1] << 8)
def enter(self):
self.send('{')
def exit(self):
self.send('}')
def erase(self):
self.send('E')
def set_tprog(self, t):
print(f"SET TPROG = {t} us")
self.send('T')
self.write_word(t)
def go(self, addr):
self.send('G')
self.write_word(addr)
echo = self.read_word()
print(f"GO={echo:04X}")
time.sleep(0.01)
def write_block(self, addr, data):
self.go(addr)
self.send('W')
self.write_word(len(data))
print(f"WRITE addr={addr:04X} len={len(data)}")
for w in data:
self.write_word(w)
def read_block(self, addr, length):
self.go(addr)
self.send('R')
self.write_word(length)
data = []
for _ in range(length):
data.append(self.read_word())
return data
# =========================
# 書き込み+検証
# =========================
def program_and_verify(pic, hexfile):
segs, mem = parse_hex_segments(hexfile)
print("ERASE")
pic.enter()
pic.erase()
pic.exit()
print("WRITE START")
pic.enter()
for addr, data in segs:
pic.write_block(addr, data)
pic.exit()
print("WRITE DONE")
print("VERIFY START")
pic.enter()
for addr, data in segs:
read = pic.read_block(addr, len(data))
for i, w in enumerate(data):
if read[i] != w:
print(f"VERIFY ERROR at {addr+i:04X}: {read[i]:04X} != {w:04X}")
pic.exit()
return False
pic.exit()
print("VERIFY OK")
return True
# =========================
# ベリファイのみ
# =========================
def verify_only(pic, hexfile):
segs, mem = parse_hex_segments(hexfile)
print("VERIFY ONLY START")
pic.enter()
for addr, data in segs:
read = pic.read_block(addr, len(data))
for i, w in enumerate(data):
if read[i] != w:
print(f"VERIFY ERROR at {addr+i:04X}: {read[i]:04X} != {w:04X}")
pic.exit()
return False
pic.exit()
print("VERIFY OK")
return True
# =========================
# メイン
# =========================
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-p", "--port", required=True)
parser.add_argument("-w", "--write")
parser.add_argument("-v", "--verify-only", help="verify only with HEX file")
parser.add_argument("-t", "--tprog", type=int,
help="programming time in microseconds")
parser.add_argument("-b", "--background", nargs='?', const=1.0, type=float,
help="background mode")
parser.add_argument("-e", "--erase", action="store_true")
parser.add_argument("-r", "--read", nargs='+')
args = parser.parse_args()
pic = PICWriter(args.port)
# ===== verify only =====
if args.verify_only:
ok = verify_only(pic, args.verify_only)
sys.exit(0 if ok else 1)
# ===== erase only =====
if args.erase and not args.write and not args.read:
print("chip erase")
pic.enter()
pic.erase()
pic.exit()
return
# ===== 書き込み =====
if args.write:
# ★ ここで一回だけ tprog 設定 ★
if args.tprog:
pic.enter()
pic.set_tprog(args.tprog)
pic.exit()
# 通常モード
if args.background is None:
program_and_verify(pic, args.write)
return
# ===== Background mode =====
interval = args.background
print(f"Background mode start (interval={interval}s)")
last_written_time = 0
while True:
try:
mtime = os.path.getmtime(args.write)
if mtime > last_written_time:
now = datetime.now()
print(now.strftime("update %Y年%m月%d日%H時%M分%S秒"))
ok = program_and_verify(pic, args.write)
if ok:
last_written_time = time.time()
time.sleep(interval)
except KeyboardInterrupt:
print("\nExit background mode")
break
except Exception as e:
print(f"ERROR: {e}")
time.sleep(2)
return
# ===== 読み込み =====
if args.read:
if len(args.read) == 1:
addr = 0
length = int(args.read[0], 0)
elif len(args.read) == 2:
addr = int(args.read[0], 0)
length = int(args.read[1], 0)
else:
print("ERROR")
return
print(f"read addr={addr:04X} len={length:04X}")
pic.enter()
data = pic.read_block(addr, length)
for i in range(0, len(data), 16):
line = data[i:i+16]
print(f"{addr+i:04X}: " +
" ".join(f"{w:04X}" for w in line))
pic.exit()
return
print("No operation specified")
if __name__ == "__main__":
main()
おまけ
C:\prj\CH552_YaPiTer>python YaPiTer.py -p com6 -r 0x2000 9
read addr=2000 len=0009
GO=2000
2000: 3FFF 3FFF 3FFF 3FFF 3FFF 3FFF 0BE1 3FFF 3FFF
0x2000番地以降を指定すればconfig memoryも読み書きできます。だからと言ってdevice IDはチェックしてません。device ID読んでターゲットCPUをオートディテクトしてデバイスコンフィクレーションリストファイルを参照してバウンドチェックしたり書き込みタイミングを自動調整できればカッコいいんですけどね。商品を作ってる訳じゃないんで。
それと自分で作っといて気に入ってるのが-bオプションです。MPLAB IDEが生成するターゲットファイルを-wオプションで指定しておけばPICkit2 programmerみたいに自動更新するんで便利です。ターゲットのオートリセット&RUNも欲しくなるよねぇ。~MCLRをいじる回路を足さないと実現できないなぁ。PWMでVPP電圧とVDD電圧を切り替えるとか?
まとめ
まぁこんなもんですね。リングバッファでフロー制御とかダブルバッファでパケット通信とかやろうと思えばやれる所はあるし削ろうと思えば削れる所はあるんだけどtporgは経験的に280μ秒前後がリミットなんで現状でも4Kwordを1.1秒程度で書けてるので僕の使い方では十分です。そもそも自分用なんでバウンダリーチェックとかリミットチェックはやってませんし、PIC16F747以外も焼けるのか試していないし、苛めるとおかしな挙動をするかもしれません。ハード&ソフト共にダメな所は自分で手直しして下さい。自分の中で重要なのはこの一歩が鶏卵問題を過去のモノとした事です。110円のCH552P板はUSB-DFUを使ってネイティブに自分自身のプログラムメモリーを焼けます。なのでWriterファイルをCH552Pに焼けばAVRとESP32に加えてPICもサクっと書き込めるのでマイコン遊びの1st takeに最適です。
誰かPIC12F509に509Writerを焼いてみて欲しいなぁ。古い石用に書き込みアライメントが1ワードか2ワードか位はオプション付けてもいいかな。逆にPIC18F2550に書く時はどうしようか。今のままでどんな石に書けるんだろう。先人達が記したDevice config dataを外部から読むとか導入するルーチンを追加すればいいんだろうけど、、、まだ見ぬ石に対応する事が合理的なんだろうか。まあPIC18F2550用は別建てでに書いちゃった方が手っ取り早いだろうな。さらに対応する石の種類を増やすのは検証のしようがないんでコノ辺でヨシとします。後になって下らない事をチョット思いついたんだけどPIC16F747に移植して自分自身をコピーする(単為増殖)モードを作ったら面白いんじゃないかな。