Deployed JTAG functionality of SpinalHDL on TinyFPGA BX and controlled it by Python.
いままで、TinyFPGA BX に PWM 機能を載せてみたり、RISC-V を JTAG デバッグしてみたりしてきましが、今回は RISC-V を使わずに JTAG 機能だけを評価してみることにしました。JTAG というとマイコンデバッグのイメージが強いですが、本来、デジタル回路を外部から観測したり制御したりするために考案されたものなのです。(と、学生時代に教わった気がする)
JTAG について
JTAG の仕組については、こちら(PDF)が詳しいです。また、SpinalHDL の JTAG 機能はこちらで説明されています。
JTAG のインターフェイスとしては、Python から制御できる、FTDI 社の FT232H ブレークアウトを使うことにしました。FTDI 社の純正品もあると思いますが、今回は調達しやすい、Adafruit 社の FT232H Breakout を使いました。なお、最新版のブレークアウトは USB Type-C コネクタになっているそうですが、私の使ったのは旧版です。また、同社のブレークアウトの EEPROM では何か専用(?)の設定がされているようですが、私は内容を消してしまったので、オリジナルがどのような設定になっているか分かりません。
FT232H ブレークアウトボードの設定
注意点としては、もし FT232H ブレークアウト上の EEPROM が空(から)になっていると、FT232H チップの電源投入時にピン設定が UART モードにアサインされ、ADBUS2 (D2) が出力モードになってしまいます。このピン D2 は TDO(JTAG TAP から JTAG インターフェイスへの信号)に使うので、FT232H のこのピンが出力モードになっていると、電気的に出力が衝突してしまい、FT232H や FPGA を壊してしまう可能性があります。必ず、FT_PROG や代替のツール(libftdi など)を使って、FT232H の動作モードを「UART 以外」に設定しておいてください。たぶん、「245 FIFO」モードが良いのではないかと思います。以下、参考画面です。(後記: ドライブ電流の制限機能もあるようです。デフォルトでは最低の 4mA になっています。これはそのままにしておくべきでしょう。)
SpinalHDL による設計
JTAG TAP については、SpinalHDL を使って簡単な設計をしてみました。FPGA 上に PWM 出力回路を載せ、その機能を JTAG で制御したり観測したりしよう、という訳です。
コードはこんな感じです。まず、PWM 回路から。
package flogics.lib.pwm
import spinal.core._
import spinal.lib._
import spinal.lib.Counter
class Pwm(size: Int) extends Component {
val io = new Bundle {
val width = in UInt (size bits)
val output = out Bool
}
val ct = Counter(size bits)
ct.increment()
io.output := ct.value < io.width
}
width というのは、PWM の出力幅(デューティ比)を決める入力です。output が、実際の PWM 出力になります。size というのは、PWM のカウンタを何ビットでインスタンス化するか、という値です。
次に JTAG TAP 回路およびトップレベルです。
import spinal.core._
import spinal.lib._
import spinal.lib.com.jtag.{Jtag, JtagTap}
import flogics.lib.pwm._
class JtagPwm_TinyFPGA_BX extends Component {
val io = new Bundle {
val CLK = in Bool
val PWM = out Bool
val TCK = in Bool
val TDI = in Bool
val TDO = out Bool
val TMS = in Bool
val USBPU = out Bool
}
val pwm_size = 8
val core_clock_domain = ClockDomain(
clock = io.CLK,
frequency = FixedFrequency(16 MHz),
config = ClockDomainConfig(
resetKind = BOOT
)
)
class MyJtagTap extends Component {
val io = new Bundle {
val jtag = slave(Jtag())
val width = out UInt(pwm_size bits)
val pwm = in Bool
}
val pwm_reg = RegNext(io.pwm).addTag(crossClockDomain)
val tap = new JtagTap(io.jtag, 8)
val idcodeArea = tap.idcode(B"x87654321")(instructionId = 4)
val widthArea = tap.write(io.width)(instructionId = 5)
val pwmArea = tap.read(pwm_reg)(instructionId = 6)
}
val jtag_area = new ClockingArea(ClockDomain(io.TCK)) {
val jtag = new MyJtagTap()
jtag.io.jtag.tms <> io.TMS
jtag.io.jtag.tdi <> io.TDI
jtag.io.jtag.tdo <> io.TDO
}
val core_area = new ClockingArea(core_clock_domain) {
io.USBPU := False
val pwm = new Pwm(
size = pwm_size
)
pwm.io.width := jtag_area.jtag.io.width
jtag_area.jtag.io.pwm := pwm.io.output
io.PWM := pwm.io.output
}
}
object JtagPwm_TinyFPGA_BX {
def main(args: Array[String]): Unit = {
SpinalVerilog(new JtagPwm_TinyFPGA_BX)
}
}
JTAG TAP 部分の肝は、以下の部分です。
val tap = new JtagTap(io.jtag, 8)
val idcodeArea = tap.idcode(B"x87654321")(instructionId = 4)
val widthArea = tap.write(io.width)(instructionId = 5)
val pwmArea = tap.read(pwm_reg)(instructionId = 6)
最初の行は、JTAG の instruction ビット幅が 8ビットであることを示しています。次は、いわゆる IDCODE instruction の ID 定義ですね。その次が PWM の width を書き込むための instruction で、最後が現在の PWM 状態を読み出す instruction 定義です。
また、コア部分と JTAG TAP は異なるクロックドメインで動作するので、レジスタ pwm_reg には .addTag(crossClockDomain) と記述することで、SpinalHDL がエラーを出さないようにしています。(今回は 1ビットの信号(pwm.io.output)なので問題ないですが、例えばカウンタを読み出すような場合には、クロックドメインをまたぐことを考慮した設計が必要になると思います。詳しくはこちら。)
Python コード
さて。FPGA 側の JTAG 設計はできましたが、これを例えばパソコンから操作するにはどうしたら良いでしょう? 世の中にはいろんな JTAG インターフェイスが売られていますが、それを Python などの言語で制御できるものは、あまり多くなさそうです。
そんな折、こんな Python ライブラリ(PyFtdi)を見つけました。Python には存在しないライブラリはないんじゃないか、というくらい、ライブラリが充実しているのが魅力ですね。残念ながら、JTAG のサンプルコードはないのですが、テストコードを調べて、コードをでっち上げてみました。
from pyftdi.jtag import JtagEngine
from pyftdi.bits import BitSequence
import time
FTDI_BOARD = 'ftdi://ftdi:232h/1'
SPEED = 5
TCK_FREQ = 1e6
MAX_WIDTH = 255
def read_pwm():
jtag.write_ir(RD_PWM)
pwm = jtag.read_dr(1)
print("PWM: 0x%x" % int(pwm))
# Define Instructions
IDCODE = BitSequence('00000100', msb=True, length=8)
WR_WIDTH = BitSequence('00000101', msb=True, length=8)
RD_PWM = BitSequence('00000110', msb=True, length=8)
# Initialize
jtag = JtagEngine(frequency=TCK_FREQ)
jtag.configure(FTDI_BOARD)
jtag.reset() # Test Logic Reset (5 times of TMS=1)
# To Shift-IR, Exit1-IR and finally move to UpdateIR
jtag.write_ir(IDCODE)
# Move to Shift-DR, repeat Shift-DR, wait 15ms, and move to Test-Logic Reset
idcode = jtag.read_dr(32)
print("IDCODE (reset): 0x%x" % int(idcode))
width = 0
dir = 1
while True:
jtag.write_ir(WR_WIDTH)
jtag.write_dr(BitSequence(width, msb=False, length=8))
width += dir * SPEED
if width >= MAX_WIDTH:
read_pwm()
dir = -1
width = MAX_WIDTH
elif width <= 0:
read_pwm()
dir = 1
width = 0
time.sleep(0.001)
実際に動かす
それでは実際にボードを結線し、パソコンに繋いで、Python コードを動かしてみましょう。
配線は次の通りです。まず、FT232H ブレークアウトから。
- ADBUS0 (D0): JTAG TCK (FT232H から出力)
- ADBUS1 (D1): JTAG TDI (FT232H から出力)
- ADBUS2 (D2): JTAG TDO (FT232H へ入力)
- ADBUS3 (D3): JTAG TMS (FT232H から出力)
次に TinyFPGA BX 側です。USB コネクタを上にして、ボードの左上側から
- GND
- JTAG TCK
- JTAG TDI
- JTAG TDO
- JTAG TMS
です。2つのボードそれぞれの GND 間も結線してくださいね。
Python から次のような出力が得られ、また、ホワホワと LED が点滅するのが確認できました!
IDCODE (reset): 0x87654321 PWM: 0x1 PWM: 0x0 PWM: 0x1 PWM: 0x0 PWM: 0x1 PWM: 0x0 PWM: 0x1 PWM: 0x0 PWM: 0x0 (続く)
オシロの波形も上げておきましょう。
信号は、
- 黄色: TCK
- 水色: TDI
- 紫: TDO
- 青: TMS
です。上の 4つがオリジナルで、下の 4つは時間軸上の拡大図です。(あ、この波形は TCK_FREQ を 100kHz で取得したものでした。すみません)
拡大図の左から、
- Test-Logic Reset 状態への遷移(jtag.reset())
- Shift-IR 状態への遷移
- IDCODE の送信(jtag.write_ir(IDCODE))
- Shift-DR 状態への遷移
- Shift-DR で 0x87654321 をシフトアウト(jtag.read_dr(32))
といった感じです。
GitHub にコードを上げておきますので、よろしければ御参考ください。
今日はここまで。おつかれさまでした!