たまりば

パソコン・インターネット パソコン・インターネット三鷹市 三鷹市

最弱のPICでTV出力
2012年11月26日 01:38

最弱のPICマイコン、PIC10F200でTV出力をしてみた。
PIC10F200はPICの中で最弱なだけでなく、今普通に店で買えるマイコンの中でも最弱ではないかと思う。



TV出力は以前も16F628Aや10F222でやったことがあった。
PICNTSC_PIC16F628_イ
16F628A、「イ」16F628A、猫耳
PICNTSC_PIC10F222_イPICNTSC_PIC10F222_猫耳
10F222、「イ」10F222、猫耳

それらと今回の10f200の性能を比べるとこんな感じ。
16F628A10F22210F200
I/Oピン数1644
処理速度1MIPS2MIPS1MIPS
FlashROM2kワード512ワード256ワード
メモリ224バイト23バイト16バイト
周辺機能色々ADCとタイマタイマのみ
色々違いはあるが、今回問題になるのは処理速度とFlashROMのみである。
16F628AはFlashが2Kワードと(10F2xxに比べれば)潤沢であったのでXORWFとNOPをベタ書きして1サイクル1ドットを出力する力技を使っていたのだが、10F222ではそうはいかないので1ドットあたりデータの読み取りと書き換えの2サイクルを使う方式をとった。
(なお後で知ったが、16F628Aの同期シリアル出力を流用して1ドット1サイクルでメモリから出力する技がある)

ベタ書きバージョン
BCF PORTA,1 ;まず黒にしておく
MOVLW 0x01 ;XOR用のマスクを読み込む
XORLW PORTA ;ここで白になる
XORLW PORTA ;ここで黒になる
NOP ;ここで黒のまま
NOP ;ここで黒のまま
XORLW PORTA ;ここで白になる
NOP ;ここで白のまま
これを表示する画像のドット数だけ並べる。例えば50×50ドットなら2500ワード。一方16F628AのFlashは2048ワード。
実際にはNOP2つをGOTO $+1にするなど多少の圧縮は効くので52×52くらいいけたりする。

メモリ読み込みバージョン
BCF GPIO,1 ;ここと
MOVLW 0x01 ;ここは同じ
btfsc DATA0, 0 ;メモリを見る
xorlw GPIO ;メモリが1ならばここで色が変わる
btfsc DATA0, 1 ;以下同様
xorlw GPIO
btfsc DATA0, 2
xorlw GPIO
これを1ライン分用意しておき、必要に応じてデータを書き換える。
解像度は半分になるが、メモリを1ワード8ビットとして使えるので効率的。
今回はFlashが少ないので当然メモリ読み込みバージョンを使ったのだが、10F222は16F628Aの2倍速で動くので都合16F628Aと同じ解像度が出せるのだが、10F200では解像度は半分になってしまった。

なおどちらの場合も、白と黒の切り替わるところで出力をXORで切り替えるので、画像データの持ち方が面倒になる。
最初は読み取るときに変換しようとしていたのだが、その分のコード(6ワード)が惜しかったのでXORした状態のデータを持つようにした。
そのかわりマクロを使ったのでソースでは一応見やすくなっている。
(ちなみにXORで出力を切り替えるのは、出力先のインピーダンス(抵抗)が十分に高い場合でないと正常に働かないらしいが、まあなんか動いてるので大丈夫だったようだ)

コードの流れは次のようになっている。
・画像の1行(走査線12ライン相当)ごとに、
 ROMの画像データテーブルを読み込み、メモリに書きだしておく
・走査線1ラインごとに、
 メモリを読んでI/Oピンに出力×24回
 水平同期信号を出力
・1画面ごとに、
 垂直同期信号を出力する
 画像データのポインタをリセットする
・15画面ごとに
 アニメーションのため画像データのポインタを1画面分ずらす

ROMから直接出力せずにメモリを経由する理由だが、ベースラインのPICはROMからの読み出しに時間がかかるためである。
メモリからワーキングレジスタにデータを読むには
movf MEM, W
の1命令1サイクルで済むのだが、
ROMからの読み出しは
(Wレジスタに対象のアドレスが入っているとして)
call READ_TABLE ;サブルーチンコール
READ_TABLE: ↓サブルーチンの場所
movwf PCL ;対象のアドレスへジャンプ
;↓PCLに書き込まれたアドレス
retlw 0xNN ;定数値を返すリターン命令
という3命令6サイクルが必要になる。

10F222では水平帰線期間を使ってROMからの読み出しをしていたのだが、速度が半分の10F200ではかなり無理があったので、1ラインを捨てて読み出しに専念している。画像の1pxごとに1ラインの黒線が入っているのがこれだ。

また、コード短縮に効果があったのが、同期信号の簡略化だ。
同期信号は前回は律儀に出していたのだが、ファミコンなど結構いい加減な同期信号を出していてそれでも大丈夫という情報を得たので、今回は垂直同期と水平同期のみにしてみた。これは難なく成功した。これによりコードサイズの大幅な削減ができた。今回もっともきつかった制限がコードサイズなので、これが無かったら1枚絵になっていただろう。

最後に回路とソースを公開する。

・回路
PICNTSC回路図
2本の抵抗で4値を出す一種の抵抗ラダーにPICの0番ピンと2番ピンをつないでいる。
ちなみになんで0と1でないかというと、この石の2番ピンはプルアップ抵抗がついておらず入力に使いづらい仕様のため優先的に出力に使いたいからである。
同軸ケーブルの右はTV内部を示す。映像信号の終端抵抗は75Ωのはず(というか実際測ったらそうだった)。電源電圧3Vの時にこれと分圧して1Vp-pが出るように抵抗値は定めたつもり。ただ、実はこれで出力したところやけに暗かったので何か間違っているかもしれない。
コンデンサはちょうど手元にあった470uFを使っているが、値は適当でよい。

・ソース
HTMLに書くにあたってタブを全角スペース2つに、半角スペース2つを全角スペース1つに置換しているので直さないと動かない。
  list  p=10F200
  #include p10f200.inc
  radix dec ;数値のデフォルトが16進だが、10進に変えておくのが好み

  ;  N/C+-v-+(GP3) ;ここに図を書いておくとブレッドボードに組むとき便利
  ;  Vdd|  |Vss
  ;  GP2|  |N/C
  ;  GP1+---+GP0

  __CONFIG _MCLRE_OFF & _CP_OFF & _WDT_OFF ;MCLRオフ、コードプロテクトオフ、WDTオフ。WDT以外はどうでもいい。

  cblock 0x10 ;メモリの宣言
    linebuf1 ;ラインバッファ×3バイト
    linebuf2
    linebuf3
    linecount ;画像1行ごとの走査線ライン数のカウンタ
    imgcount ;画像データ読み出し用のポインタ
    wcount ;ウェイト用のカウンタ
    blankloopcount ;垂直同期中の走査線ライン数カウンタ
    framecount ;アニメーション用にフレーム数(正しくはフィールド数)をカウント
  endc

#define lineperrow 11 ;絵の1行あたりの走査線数-1

waitx macro cycle ;指定サイクル待つ; 2ワード
  movlw ((cycle)-5)/3
  call wait3w4 - (((cycle)-5)%3)
  endm

imgdat macro dat1, dat2, dat3 ;画像データをXORされた形式で保存するためのマクロ
  retlw low(dat1 ^ ((dat1<<1) | (dat2>>7)))
  retlw low(dat2 ^ ((dat2<<1) | (dat3>>7)))
  retlw low(dat3 ^ (dat3<<1))
  endm

init ;初期化
  org 0x00
  
  movwf  OSCCAL ;内蔵発振器を較正。
    ;NTSC信号のタイミングはそれなりにシビアなので、較正しないとTVに映らない。
    ;とはいえ、63.5usを64usで代用できる程度にはルーズ。
  movlw b'10011000' ;以下3つのレジスタの初期化値を兼ねる値
  option ;x00xxxxx ;
  ;    ^/GPWU: Don'tCare
  ;    ^/GPPU=0: PullUp ON ←スイッチ入力も視野に入れていたので
  ;     ^T0CS=0: Fosc/4,GP2=I/O ←結局2ピンしか使ってないので要らないっちゃ要らない
  ;     ^T0SE Don'tCare
  ;      ^PSA Don'tCare
  ;      ^^^PS2:0 Don'tCare

  movwf imgcount ; 83<=x<=BC or C3<=x<=FC でかつ下6bitが3で割れる数
    ;これであれば、最初の1フレームは中途半端になるが2フレーム目からは正常。
    ;この条件を満たさないと、画像の切り替え判定にかからず正常に動作しない。

  tris GPIO ;xxxxx000: 全部出力
  
  ;linecount: 最初の1回のみライン数が増えるだけなので、初期化不要
  ;framecount: 最初の1枚の画像のみ長く表示されるだけなので、初期化不要
  ;blankloopcount: 最初の1フレームのみ同期信号の長さが異常になるだけなので、初期化不要

main ;メインルーチン

;ラインごとのタイムテーブル。0~63サイクル、1サイクル1us。
;電圧値: 0:同期信号、低:黒、高:白
;0
;1: 0 : 水平同期信号始
;6: 低 : 水平同期信号終
;13: 低/高 : dot0始
;15: 低/高 : dot0終、dot1始
;...
;59: 低/高 : dot23始
;61: 低 :dot23終
;63

;line 24
drawloop ;描画ループ。11ライン分繰り返す
  bsf GPIO,0 ;6 ;電圧低:水平同期終
  call wait4 ;7-10 ←この数字は1ライン内のサイクル数カウント
  movlw 2 ;11
  btfsc linebuf1,0
  xorwf GPIO,F ;13
  btfsc linebuf1,1
  xorwf GPIO,F
  btfsc linebuf1,2
  xorwf GPIO,F
  btfsc linebuf1,3
  xorwf GPIO,F
  btfsc linebuf1,4
  xorwf GPIO,F
  btfsc linebuf1,5
  xorwf GPIO,F
  btfsc linebuf1,6
  xorwf GPIO,F
  btfsc linebuf1,7
  xorwf GPIO,F
  btfsc linebuf2,0
  xorwf GPIO,F
  btfsc linebuf2,1
  xorwf GPIO,F
  btfsc linebuf2,2
  xorwf GPIO,F
  btfsc linebuf2,3
  xorwf GPIO,F
  btfsc linebuf2,4
  xorwf GPIO,F
  btfsc linebuf2,5
  xorwf GPIO,F
  btfsc linebuf2,6
  xorwf GPIO,F
  btfsc linebuf2,7
  xorwf GPIO,F
  btfsc linebuf3,0
  xorwf GPIO,F
  btfsc linebuf3,1
  xorwf GPIO,F
  btfsc linebuf3,2
  xorwf GPIO,F
  btfsc linebuf3,3
  xorwf GPIO,F
  btfsc linebuf3,4
  xorwf GPIO,F
  btfsc linebuf3,5
  xorwf GPIO,F
  btfsc linebuf3,6
  xorwf GPIO,F
  btfsc linebuf3,7
  xorwf GPIO,F
  nop ;60
  bcf GPIO,1 ;61 ;電圧低
  goto $+1 ;62,63
  nop ;0
  bcf GPIO,0 ;1 ;電圧0:水平同期始
  nop ;2
  decfsz linecount,F ;3
  goto drawloop ;4,5

endofrow ;1行の終わり。次の行のために画像をラインバッファに読み込む。
  bcf STATUS,C ;5
  bsf GPIO,0 ;6

  call readgraphic
  movwf linebuf1 ;14
;  rlf linebuf1,W ;画像をそのまま保存していたときはここでXORをとっていた。
;  xorwf linebuf1,F ;17
  
  call readgraphic
  movwf linebuf2
;  rlf linebuf2,W
;  xorwf linebuf2,F ;28
  
  call readgraphic
  movwf linebuf3
;  rlf linebuf3,W
;  xorwf linebuf3,F ;39
  
  movlw lineperrow
  movwf linecount ;41
  ;~62
  waitx 62-41+6 ;62-41 ;サイクル数カウントがXORの分がそのままなので、+6で調整。

  movlw 0x3F ;63 ;
  andwf imgcount,W ;0 ;画像ポインタをチェックするためにWに。
  bcf GPIO,0 ;1 ;ここで電圧:0にして水平同期始
  btfsc STATUS,Z ;2 ;閑話休題、画像ポインタの下6bitが0なら、
  goto vblank ;3,4 ;画面の下端なので垂直同期へ。
  goto drawloop ;4,5 ;さもなくばループ。

  ;line 263
vblank

  bsf blankloopcount,2 ;5 ;空の走査線のカウンタ。(0であることを前提に)4を代入。
blankloop1 ;line 1-4

  bsf GPIO,0 ;6 ;電圧低:水平同期終
  waitx 64-6
  bcf GPIO,0 ;1=64 ;電圧0:垂直同期始
  movlw 15 ;2 ;下で使う値。ちょうど空いていたのでここでWに置いておく。
  decfsz blankloopcount,F ;3 ;垂直同期前の空の走査線をカウント。
  goto blankloop1 ;4,5 ;4ライン数えたら垂直同期へ。

;垂直同期信号。3ライン間L、切込みパルス省略。

  ;アニメーション
  decf framecount,F ;フレームカウンタを減らし、
  btfsc STATUS,Z ;0なら、フレームカウンタを15にリセット
  movwf framecount ;10 ;上でWに置いた値をここで使う。
  movlw 0x40 ;
  btfsc STATUS,Z ;上で0ならここも0
  xorwf imgcount,F ;0なら画像カウンタの6bit目を反転

  ;
  movlw 60
  addwf imgcount,F ;12 ;画像カウンタを画像1枚分戻す
  
  ;~64=0
  ;line 5
  ;~64=0
  ;line 6
  ;~64=0
  ;line 7
  ;~2
  waitx 64+64+64+2-12+1 ;3ライン分のウェイト

  bsf blankloopcount, 4 ;空の走査線のカウンタ。(0であることを前提に)16を代入。
blankloop2 ;line 7~24
  nop ;5
  bsf GPIO,0 ;6 ;電圧低:垂直/水平同期終
  ;~64
  waitx 64-6
  bcf GPIO,0 ;1 ;電圧0:水平同期始
  
  decfsz blankloopcount,F ;2 ;垂直同期前の空の走査線をカウント。
  goto blankloop2 ;3,4 ;16ライン数えたら最初に戻る
  goto drawloop ;4,5 ;line 24

;;メインルーチンここまで;;

wait3w6 ;下に同じく
  nop
wait3w5 ;↓に加えてもう1サイクルのウェイト
  nop
wait3w4 ;1<=W<=255の時、(3*W+4)サイクルのウェイト。W=0なら256扱い。
  movwf wcount
  decfsz wcount,F
  goto $-1
wait4 ;ここにcallすると、callとretlwで計4サイクルのウェイトになる
  retlw 0

  org 0x80
graphicdata1 ;3byte × 20行 = 60byte
;画像データ。
;下6bitが000000であることを描画の終了判定に使っていることと、
;0xFF番地は発振器の補正値に使われていることにより、
;下から上の順に記録している。
;また、出力の後にラインバッファの更新をしているため1行ずれて、最初が最上段で次から最下段~最上段の下の順。
;逆にしてバッファ更新後に出力とすればよかったのかな。もう遅いけど。
  imgdat 0xC0, 0x9F, 0xFF ;最上段
  imgdat 0x20, 0x10, 0x7C ;最下段~
  imgdat 0x24, 0x0B, 0x71
  imgdat 0x2E, 0x44, 0xC8
  imgdat 0x0F, 0xE8, 0x30
  imgdat 0x0E, 0x50, 0x4F
  imgdat 0x0E, 0x00, 0x3F
  imgdat 0x6C, 0xA0, 0x3F
  imgdat 0xAD, 0x60, 0x3F
  imgdat 0x40, 0x41, 0xFF
  imgdat 0x9F, 0xBF, 0xFF
  imgdat 0x38, 0xC1, 0xFF
  imgdat 0x5A, 0x83, 0xFF
  imgdat 0x7D, 0xC3, 0xFF
  imgdat 0x27, 0x03, 0xFF
  imgdat 0x25, 0x07, 0xFF
  imgdat 0x1C, 0x87, 0xFF
  imgdat 0x08, 0x07, 0xFF
  imgdat 0x00, 0x0F, 0xFF
  imgdat 0x00, 0x0F, 0xFF ;~最上段の下

;ちなみに画像をそのまま保存していたときはこう
;  dt 0xC0, 0x9F, 0xF2 ;最上段
;  dt 0x20, 0x10, 0x7F, 0x24, 0x08, 0x7F, 0x2E, 0x44, 0x7F, 0x0F, 0xE4, 0x7F ;最下段~
;  dt 0x0E, 0x58, 0xFF, 0x0E, 0x20, 0xFF, 0x6C, 0xBC, 0xFF, 0xAD, 0x43, 0xFF
;  dt 0x40, 0x60, 0x7F, 0x9F, 0xA0, 0x7F, 0x38, 0xC0, 0x7F, 0x5A, 0x80, 0x7F
;  dt 0x7D, 0xD0, 0xBF, 0x27, 0x09, 0x5F, 0x25, 0x07, 0xAF, 0x1C, 0x87, 0xD7
;  dt 0x08, 0x07, 0xE9, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0 ;~最上段の下

  org 0xBD
readgraphic ;画像データ読み取りコード。ちょうど2枚の画像データの隙間に収まるサイズのコードだったのでここに入れた。
  decf imgcount,F
  movf imgcount,W
  movwf PCL

  org 0xC0
graphicdata2 ;画像2枚目
  imgdat 0xC0, 0x9F, 0xF2 ;最上段
  imgdat 0x20, 0x10, 0x7F ;最下段~
  imgdat 0x24, 0x08, 0x7F
  imgdat 0x2E, 0x44, 0x7F
  imgdat 0x0F, 0xE4, 0x7F
  imgdat 0x0E, 0x58, 0xFF
  imgdat 0x0E, 0x20, 0xFF
  imgdat 0x6C, 0xBC, 0xFF
  imgdat 0xAD, 0x43, 0xFF
  imgdat 0x40, 0x60, 0x7F
  imgdat 0x9F, 0xA0, 0x7F
  imgdat 0x38, 0xC0, 0x7F
  imgdat 0x5A, 0x80, 0x7F
  imgdat 0x7D, 0xD0, 0xBF
  imgdat 0x27, 0x09, 0x5F
  imgdat 0x25, 0x07, 0xAF
  imgdat 0x1C, 0x87, 0xD7
  imgdat 0x08, 0x07, 0xE9
  imgdat 0x00, 0x0F, 0xF0
  imgdat 0x00, 0x0F, 0xF0 ;~最上段の下

;3ワードの隙間にちょうど入るコードだったのでここに入れたが、上の4サイクルウェイトとあわせて使うコード。
;4サイクルウェイトに加えてgotoで2サイクルづつウェイトを増やしている。
;あれでもよく見たらここ使われてない…。コードの最適化のせいで中途半端なウェイトを使う場所が無くなったようだ。
wait10 ;call wait10 1,2
  goto $+1 ;3,4
wait8
  goto $+1 ;5,6
wait6
  goto wait4 ;7,8

  end
  

  • ハロウィー?ンの正規表現
    2012年11月01日 01:54

    ハロウィーンですね。
    …あ、昔の1日って日の出から始まるんでまだeveだと思うんですよ。

    それはともかく、ハロウィーンといえばJack-O'-Lanternですね。
    これ自分は「ジャックオーランタン」って書くんですが、
    真ん中が「オー」じゃなくて「オ」や「オウ」だったり、
    ジャックのクとつながって「ジャッコーランタン」になったり、
    もっと縮めて「ジャコランタン」ってのもありますね。
    「ランタン」は「ランターン」派もいたり、
    「・」で区切って「ジャック・オー・ランタン」にしたり、
    あと「-O'-」の部分を無くしちゃった「ジャックランタン」って言い方もあるっぽいです。

    こういう表記ゆれに対抗する手段といえば正規表現。
    例えば「ハロウィン」と「ハロウィーン」にマッチさせたければ/ハロウィー?ン/のように。
    ではJack-O'-Lanternのカナ表記すべてにマッチさせる正規表現はどうなるのか。
    考えてみるとなかなか手ごたえがあって楽しかったので正規表現好きの方は下の解答を見る前にチャレンジしてみるといいと思います。

    まず、上のパターンをまとめると、ありうるパターンは次の26種類。
    ジャックオーランターン
    ジャックオーランタン
    ジャックオウランターン
    ジャックオウランタン
    ジャックオランターン
    ジャックオランタン
    ジャッコーランターン
    ジャッコーランタン
    ジャッコウランターン
    ジャッコウランタン
    ジャッコランターン
    ジャッコランタン
    ジャコーランターン
    ジャコーランタン
    ジャコウランターン
    ジャコウランタン
    ジャコランターン
    ジャコランタン
    ジャック・オー・ランターン
    ジャック・オー・ランタン
    ジャック・オウ・ランターン
    ジャック・オウ・ランタン
    ジャック・オ・ランターン
    ジャック・オ・ランタン
    ジャックランターン
    ジャックランタン
    また、注意すべきパターンとして、
    「ジャコ」以外の形で「ッ」の省略は不可。例: ジャクオランタン
    「・」を「O'」の片側だけに入れるのは不可。例: ジャックオ・ランタン
    とすべきであろう。
    ジャコ・ランタンが許されるかどうかは微妙。ジャックオ・ランタンに類するものとして不可としておく。
    ジャック・ランタンは許されるべきな気もするが、今回は気づいたのが正規表現を考えちゃった後だったので…。

    こういう条件のもとで考えてみます。
    まず後半は簡単。
    ランター?ン
    少しづつ潰して行きます。「オ」「オー」「オウ」の部分はこれでいけます。
    オ[ーウ]?
    「ッ」の省略は複雑です。「O'」の両側の「・」はとりあえずおいておくとして、「クオー」と「コー」のORで考えます。「クオー」の時は「ッ」が必須、「コー」の時は省略可ですので、こう書けます。
    (ックオ|ッ?コ)
    最後に「・」です。あるなら両方に無ければいけない、そんなときには前に出てきたものにマッチする後方参照です。
    (・?)オー?\1
    これで1個めの「・」があるならそれを、無ければ空文字列をキャプチャし、それを2個めにマッチさせることができます。
    あとはジャックランタンに対応するために「オー」系列を囲って「?」を付け、まとめたものがこちらです。
    /ジャ(ック((・?)オ[ーウ]?\3)?|ッ?コ[ーウ]?)ランター?ン/
    「[ーウ]」は「・」との兼ね合いで2箇所に分かれているそれぞれに付ける必要があります。それと括弧が増えたので後方参照の数字が変わっていることに注意。

    さて、これで一応正しくマッチしますが、「[ーウ]」と「ッ」が2箇所にあるのがちょっと美しくないですね。
    (追記: 以下に書いてある正規表現は間違っていました。下記の追記参照)
    こんな時には必殺のアトミックゼロ幅アサーションです。名前がカッコいいです。
    アサーションという言葉の意味はいまいちよく分からないのですが、どうやら「前提条件のチェック」のような意味っぽいです。
    ここでは「ッ」の省略可否のチェックに使います。
    1文字先を読んで「コ」であるなら「ッ」を省略可。すなわち「「ッ」である」か「空文字列であり次が「コ」である」となります。こうです。
    ジャ(ッ|(?=コ))
    こうすることによりこの後の場合分けが不要になり、最終的にこうなります。
    /ジャ(ッ|(?=コ))(ク(・?)オ?|コ)[ーウ]?\3ランター?ン/
    「コ」が2回出てきていますが、最初の「(?=コ)」の部分は先読みに使っただけで実際にはマッチしないので、実際のマッチのためにもう1個必要になります。
    これを除いて複数出てきている文字は無くなったので、美しくなったと思います。複雑で分かりにくくなったという批判は受け付けません。

    ところでこれ、「(ク(・?)オ?|コ)」の部分でORの左側を通れば「・」または空文字列がキャプチャされますが右側では何もキャプチャされない気がします。
    これを「\3」で呼び出しているのは果たして大丈夫なのだろうかとちょっと不安になりますが、とりあえず実験に使ったEmEditorでは成功するようです。

    (追記)
    …えー、美しくなったとか何とか言ってますが、思いっきり間違ってました。
    以下解説しますが、どこが間違ってるのか探すのも面白いと思うので答えを見る前に考えてみるとよいでしょう。
    ちなみに実は書いてすぐ間違いに気付いたんですが、どうにかして直せないか考えてるうちにこんなに遅くなってしまいました。
    結局、直してはみたものの美しくなくなってしまって、アトミックゼロ幅アサーションを使う意味が無くなってしまいました。
    あと、一応直した表現を書いてたんですが、タブに開いたまま数日放っておいたらブラウザ再起動で消えてやる気ダウン。
    まあそんなわけでどこが間違っていたというと、「(ク(・?)オ?|コ)」の部分。
    これで「ク」「ク・オ」「クオ」「コ」にマッチするんですが、その次の「[ーウ]?」が「」「ウ」「ー」にマッチするせいで、「ジャックウランタン」などにマッチしてしまいます。
    これをどうにか直そうとすると、このあたり複雑に入り組んでいるのでどうしても場合分けがいりそうです。
    否定後読みを使って「[ーウ]?」を「ク」が前に無いときにだけマッチさせるか
    /ジャ(ッ|(?=コ))(ク(・?)オ?|コ)((?<!ク)[ーウ]?)?\3ランター?ン/
    「ク」だけ別扱いにするか
    /ジャ(ッ|(?=コ))(ク|(ク(・?)オ|コ)[ーウ]?)\4ランター?ン/
    どちらにせよ同じ文字が複数出現するのは避けられない上に、最初のものより文字数が長くなってしまいます。
    ちょっと直すだけでジャック・ランター?ンにマッチするのが利点かなとも思ったんですが、
    /ジャ(ッ|(?=コ))(ク(・?)|(ク(・?)オ|コ)[ーウ]?)\5ランター?ン/
    最初のでも普通に可能でした。
    /ジャ(ック((・?)(オ[ーウ]?\3)?)?|ッ?コ[ーウ]?)ランター?ン/
      
    タグ :プログラム