diff --git a/dif/const.go b/dif/const.go
index a35d09fa2cae5b08266bc53a7fb9f0e499fb705a..cf38229f5f072c1a3dd0d715e7f2788ba9a29d37 100644
--- a/dif/const.go
+++ b/dif/const.go
@@ -17,4 +17,18 @@ const (
 
 	anHeader = 0xc4 // analog frame header marker
 	incFrame = 0xc3 // incomplete frame marker
+
+)
+
+const (
+	MaxEventSize = (hardrocV2SLCFrameSize+1)*MaxNumASICs + (20*ASICMemDepth+2)*MaxNumASICs + 3 + MaxFwHeaderSize + 2 + MaxAnalogDataSize + 50
+
+	MaxAnalogDataSize = 1024*64*2 + 20
+	MaxFwHeaderSize   = 50
+	MaxNumASICs       = 48  // max number of hardrocs per dif that the system can handle
+	MaxNumDIFs        = 200 // max number of difs that the system can handle
+	ASICMemDepth      = 128 // memory depth of one asic . 128 is for hardroc v1
+
+	hardrocV2SLCFrameSize = 109
+	microrocSLCFrameSize  = 74
 )
diff --git a/dif/device.go b/dif/device.go
new file mode 100644
index 0000000000000000000000000000000000000000..4429f1db5d8b2687b0116b5a99097142087eb7f3
--- /dev/null
+++ b/dif/device.go
@@ -0,0 +1,257 @@
+// Copyright 2020 The go-lpc Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dif
+
+import (
+	"encoding/binary"
+	"io"
+
+	"github.com/ziutek/ftdi"
+	"golang.org/x/xerrors"
+)
+
+type ftdiDevice interface {
+	Reset() error
+
+	SetBitmode(iomask byte, mode ftdi.Mode) error
+	SetFlowControl(flowctrl ftdi.FlowCtrl) error
+	SetLatencyTimer(lt int) error
+	SetWriteChunkSize(cs int) error
+	SetReadChunkSize(cs int) error
+	PurgeBuffers() error
+
+	io.Writer
+	io.Reader
+	io.Closer
+}
+
+type device struct {
+	vid uint16     // vendor ID
+	pid uint16     // product ID
+	ft  ftdiDevice // handle to the FTDI device
+}
+
+var (
+	ftdiOpen = ftdiOpenImpl
+)
+
+func ftdiOpenImpl(vid, pid uint16) (ftdiDevice, error) {
+	dev, err := ftdi.OpenFirst(int(vid), int(pid), ftdi.ChannelAny)
+	return dev, err
+}
+
+func newDevice(vid, pid uint16) (*device, error) {
+	ft, err := ftdiOpen(vid, pid)
+	if err != nil {
+		return nil, xerrors.Errorf("could not open FTDI device (vid=0x%x, pid=0x%x): %w", vid, pid, err)
+	}
+
+	dev := &device{vid: vid, pid: pid, ft: ft}
+	err = dev.init()
+	if err != nil {
+		ft.Close()
+		return nil, xerrors.Errorf("could not initialize FTDI device (vid=0x%x, pid=0x%x): %w", vid, pid, err)
+	}
+
+	return dev, nil
+}
+
+func (dev *device) init() error {
+	var err error
+
+	err = dev.ft.Reset()
+	if err != nil {
+		return xerrors.Errorf("could not reset USB: %w", err)
+	}
+
+	err = dev.ft.SetBitmode(0, ftdi.ModeBitbang)
+	if err != nil {
+		return xerrors.Errorf("could not disable bitbang: %w", err)
+	}
+
+	err = dev.ft.SetFlowControl(ftdi.FlowCtrlDisable)
+	if err != nil {
+		return xerrors.Errorf("could not disable flow control: %w", err)
+	}
+
+	err = dev.ft.SetLatencyTimer(2)
+	if err != nil {
+		return xerrors.Errorf("could not set latency timer to 2: %w", err)
+	}
+
+	err = dev.ft.SetWriteChunkSize(0xffff)
+	if err != nil {
+		return xerrors.Errorf("could not set write chunk-size to 0xffff: %w", err)
+	}
+
+	err = dev.ft.SetReadChunkSize(0xffff)
+	if err != nil {
+		return xerrors.Errorf("could not set read chunk-size to 0xffff: %w", err)
+	}
+
+	if dev.pid == 0x6014 {
+		err = dev.ft.SetBitmode(0, ftdi.ModeReset)
+		if err != nil {
+			return xerrors.Errorf("could not reset bit mode: %w", err)
+		}
+	}
+
+	err = dev.ft.PurgeBuffers()
+	if err != nil {
+		return xerrors.Errorf("could not purge USB buffers: %w", err)
+	}
+
+	return err
+}
+
+func (dev *device) close() error {
+	return dev.ft.Close()
+}
+
+func (dev *device) usbRegRead(addr uint32) (uint32, error) {
+	a := (addr | 0x4000) & 0x7fff
+	p := []byte{uint8(a>>8) & 0xff, uint8(a>>0) & 0xff, 0, 0}
+
+	n, err := dev.ft.Write(p[:2])
+	switch {
+	case err != nil:
+		return 0, xerrors.Errorf("could not write USB addr 0x%x: %w", addr, err)
+	case n != len(p[:2]):
+		return 0, xerrors.Errorf("could not write USB addr 0x%x: %w", addr, io.ErrShortWrite)
+	}
+
+	_, err = io.ReadFull(dev.ft, p)
+	if err != nil {
+		return 0, xerrors.Errorf("could not read register 0x%x: %w", addr, err)
+	}
+
+	v := binary.BigEndian.Uint32(p)
+	return v, nil
+}
+
+func (dev *device) usbCmdWrite(cmd uint32) error {
+	addr := cmd | 0x8000 // keep only 14 LSB, write, so bit 14=0,register mode, so bit 15=0
+	buf := []byte{uint8(addr>>8) & 0xff, uint8(addr>>0) & 0xff}
+
+	n, err := dev.ft.Write(buf)
+	switch {
+	case err != nil:
+		return xerrors.Errorf("could not write USB command 0x%x: %w", cmd, err)
+	case n != len(buf):
+		return xerrors.Errorf("could not write USB command 0x%x: %w", cmd, io.ErrShortWrite)
+	}
+
+	return nil
+}
+
+func (dev *device) usbRegWrite(addr, v uint32) error {
+	var (
+		a = addr & 0x3fff
+		p = make([]byte, 6)
+	)
+
+	binary.BigEndian.PutUint16(p[:2], uint16(a))
+	binary.BigEndian.PutUint32(p[2:], uint32(v))
+	and0xff(p)
+
+	n, err := dev.ft.Write(p)
+	switch {
+	case err != nil:
+		return xerrors.Errorf("could not write USB register (0x%x, 0x%x): %w", addr, v, err)
+	case n != len(p):
+		return xerrors.Errorf("could not write USB register (0x%x, 0x%x): %w", addr, v, io.ErrShortWrite)
+	}
+	return nil
+}
+
+func (dev *device) setChipTypeRegister(v uint32) error {
+	return dev.usbRegWrite(0x00, v)
+}
+
+func (dev *device) setDIFID(v uint32) error {
+	return dev.usbRegWrite(0x01, v)
+}
+
+func (dev *device) setControlRegister(v uint32) error {
+	return dev.usbRegWrite(0x03, v)
+}
+
+func (dev *device) getControlRegister() (uint32, error) {
+	return dev.usbRegRead(0x03)
+}
+
+func (dev *device) difCptReset() error {
+	const addr = 0x03
+	v, err := dev.usbRegRead(addr)
+	if err != nil {
+		return xerrors.Errorf("could not read register 0x%x", addr)
+	}
+
+	v |= 0x2000
+	err = dev.usbRegWrite(addr, v)
+	if err != nil {
+		return xerrors.Errorf("could not write to register 0x%x", addr)
+	}
+
+	v &= 0xffffdfff
+	err = dev.usbRegWrite(addr, v)
+	if err != nil {
+		return xerrors.Errorf("could not write to register 0x%x", addr)
+	}
+
+	return nil
+}
+
+func (dev *device) setPwr2PwrARegister(v uint32) error  { return dev.usbRegWrite(0x40, v) }
+func (dev *device) setPwrA2PwrDRegister(v uint32) error { return dev.usbRegWrite(0x41, v) }
+func (dev *device) setPwrD2DAQRegister(v uint32) error  { return dev.usbRegWrite(0x42, v) }
+func (dev *device) setDAQ2PwrDRegister(v uint32) error  { return dev.usbRegWrite(0x43, v) }
+func (dev *device) setPwrD2PwrARegister(v uint32) error { return dev.usbRegWrite(0x44, v) }
+
+func (dev *device) setEventsBetweenTemperatureReadout(v uint32) error {
+	return dev.usbRegWrite(0x55, v)
+}
+
+func (dev *device) setAnalogConfigureRegister(v uint32) error {
+	return dev.usbRegWrite(0x60, v)
+}
+
+func (dev *device) usbFwVersion() (uint32, error) {
+	return dev.usbRegRead(0x100)
+}
+
+func (dev *device) hardrocFlushDigitalFIFO() error {
+	return nil
+}
+
+func (dev *device) hardrocStopDigitalAcquisitionCommand() error {
+	return dev.usbCmdWrite(0x02)
+}
+
+func (dev *device) hardrocSLCStatusRead() (uint32, error) {
+	return dev.usbRegRead(0x06)
+}
+
+func (dev *device) hardrocCmdSLCWrite() error {
+	return dev.usbCmdWrite(0x01)
+}
+
+func (dev *device) hardrocCmdSLCWriteCRC(v uint16) error {
+	p := make([]byte, 2)
+	binary.BigEndian.PutUint16(p, v)
+	_, err := dev.ft.Write(p)
+	return err
+}
+
+func (dev *device) cmdSLCWriteSingleSLCFrame(p []byte) error {
+	_, err := dev.ft.Write(p)
+	return err
+}
+
+func and0xff(p []byte) {
+	for i := range p {
+		p[i] &= 0xff
+	}
+}
diff --git a/dif/device_test.go b/dif/device_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b6237a87e2305b4ba9e94e70e758be0c1619ff43
--- /dev/null
+++ b/dif/device_test.go
@@ -0,0 +1,145 @@
+// Copyright 2020 The go-lpc Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dif
+
+import (
+	"io"
+	"testing"
+
+	"golang.org/x/xerrors"
+)
+
+func TestFTDIOpen(t *testing.T) {
+	dev, err := ftdiOpenImpl(0, 0)
+	if err == nil {
+		_ = dev.Close()
+	}
+}
+
+type ierr struct {
+	n int
+	e error
+}
+
+type failingRW struct {
+	rs []ierr
+	ws []ierr
+}
+
+func (rw *failingRW) Read(p []byte) (int, error) {
+	i := len(rw.rs)
+	rs := rw.rs[i-1]
+	rw.rs = rw.rs[:i-1]
+	return rs.n, rs.e
+}
+
+func (rw *failingRW) Write(p []byte) (int, error) {
+	i := len(rw.ws)
+	ws := rw.ws[i-1]
+	rw.ws = rw.ws[:i-1]
+	return ws.n, ws.e
+}
+
+func TestDevice(t *testing.T) {
+	ftdiOpen = ftdiOpenTest
+	defer func() {
+		ftdiOpen = ftdiOpenImpl
+	}()
+
+	dev, err := newDevice(0x1, 0x2)
+	if err != nil {
+		t.Fatalf("could not create fake device: %+v", err)
+	}
+	defer dev.close()
+
+	var (
+		rw failingRW
+		ft = fakeDevice{buf: &rw}
+	)
+	dev.ft = &ft
+
+	for _, tc := range []struct {
+		name string
+		f    func() error
+		want error
+	}{
+		{
+			name: "usbRegRead-eof",
+			f: func() error {
+				rw.ws = append(rw.ws, ierr{0, io.EOF})
+				_, err := dev.usbRegRead(0x1234)
+				return err
+			},
+			want: xerrors.Errorf("could not write USB addr 0x%x: %w", 0x1234, io.EOF),
+		},
+		{
+			name: "usbRegRead-short-write",
+			f: func() error {
+				rw.ws = append(rw.ws, ierr{1, nil})
+				_, err := dev.usbRegRead(0x1234)
+				return err
+			},
+			want: xerrors.Errorf("could not write USB addr 0x%x: %w", 0x1234, io.ErrShortWrite),
+		},
+		{
+			name: "usbRegRead-err-read",
+			f: func() error {
+				rw.ws = append(rw.ws, ierr{2, nil})
+				rw.rs = append(rw.rs, ierr{2, io.ErrUnexpectedEOF})
+				_, err := dev.usbRegRead(0x1234)
+				return err
+			},
+			want: xerrors.Errorf("could not read register 0x%x: %w", 0x1234, io.ErrUnexpectedEOF),
+		},
+		{
+			name: "usbCmdWrite-eof",
+			f: func() error {
+				rw.ws = append(rw.ws, ierr{0, io.EOF})
+				return dev.usbCmdWrite(0x1234)
+			},
+			want: xerrors.Errorf("could not write USB command 0x%x: %w", 0x1234, io.EOF),
+		},
+		{
+			name: "usbCmdWrite-short-write",
+			f: func() error {
+				rw.ws = append(rw.ws, ierr{1, nil})
+				return dev.usbCmdWrite(0x1234)
+			},
+			want: xerrors.Errorf("could not write USB command 0x%x: %w", 0x1234, io.ErrShortWrite),
+		},
+		{
+			name: "usbRegWrite-eof",
+			f: func() error {
+				rw.ws = append(rw.ws, ierr{0, io.EOF})
+				return dev.usbRegWrite(0x1234, 0x255)
+			},
+			want: xerrors.Errorf("could not write USB register (0x%x, 0x%x): %w", 0x1234, 0x255, io.EOF),
+		},
+		{
+			name: "usbRegWrite-short-write",
+			f: func() error {
+				rw.ws = append(rw.ws, ierr{1, nil})
+				return dev.usbRegWrite(0x1234, 0x255)
+			},
+			want: xerrors.Errorf("could not write USB register (0x%x, 0x%x): %w", 0x1234, 0x255, io.ErrShortWrite),
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			got := tc.f()
+			switch {
+			case got == nil && tc.want == nil:
+				// ok
+			case got == nil && tc.want != nil:
+				t.Fatalf("got=%v, want=%v", got, tc.want)
+			case got != nil && tc.want != nil:
+				if got, want := got.Error(), tc.want.Error(); got != want {
+					t.Fatalf("got= %v\nwant=%v", got, want)
+				}
+			case got != nil && tc.want == nil:
+				t.Fatalf("got=%+v\nwant=%v", got, tc.want)
+			}
+		})
+	}
+}
diff --git a/dif/readout.go b/dif/readout.go
new file mode 100644
index 0000000000000000000000000000000000000000..aa25b8737103e11cdcf48df3b64da6dac57709f8
--- /dev/null
+++ b/dif/readout.go
@@ -0,0 +1,309 @@
+// Copyright 2020 The go-lpc Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dif
+
+import (
+	"fmt"
+	"io"
+	"time"
+
+	"github.com/go-daq/tdaq/log"
+	"github.com/go-lpc/mim/internal/crc16"
+	"golang.org/x/xerrors"
+)
+
+type asicKind uint32
+
+const (
+	microrocASIC asicKind = 11
+	hardrocASIC  asicKind = 2
+)
+
+type Readout struct {
+	msg    log.MsgStream
+	dev    *device
+	name   string
+	difID  uint32
+	asic   asicKind // asic type
+	nasics int      // number of asics
+	ctlreg uint32   // control register
+	curSC  uint32   // current slow-control status
+	reg    struct {
+		p2pa   uint32 // power to power A
+		pa2pd  uint32 // power A to power D
+		pd2daq uint32 // power D to DAQ
+		daq2pd uint32 // DAQ to power D
+		pd2pa  uint32 // power D to power A
+	}
+	temp [2]float32 // temperatures
+}
+
+func NewReadout(name string, prodID uint32, msg log.MsgStream) (*Readout, error) {
+	dev, err := newDevice(0x0403, uint16(prodID))
+	if err != nil {
+		return nil, xerrors.Errorf("could not find DIF driver (%s, 0x%x): %w", name, prodID, err)
+	}
+
+	rdo := &Readout{
+		msg:    msg,
+		dev:    dev,
+		name:   name,
+		asic:   hardrocASIC,
+		nasics: MaxNumASICs,
+		ctlreg: 0x80181b00, // ILC CCC
+	}
+	rdo.reg.p2pa = 0x3e8
+	rdo.reg.pa2pd = 0x3e6
+	rdo.reg.pd2daq = 0x4e
+	rdo.reg.daq2pd = 0x4e
+	rdo.reg.pd2pa = 0x4e
+	_, err = fmt.Sscanf(name, "FT101%d", &rdo.difID)
+	if err != nil {
+		_ = dev.close()
+		return nil, xerrors.Errorf("could not find DIF-id from %q: %w", name, err)
+	}
+
+	return rdo, nil
+}
+
+func (rdo *Readout) close() error {
+	err := rdo.dev.close()
+	if err != nil {
+		return xerrors.Errorf("could not close DIF driver: %w", err)
+	}
+	return nil
+}
+
+func (rdo *Readout) start() error {
+	return rdo.dev.hardrocFlushDigitalFIFO()
+}
+
+func (rdo *Readout) stop() error {
+	var err error
+
+	err = rdo.dev.hardrocFlushDigitalFIFO()
+	if err != nil {
+		return xerrors.Errorf("could not flush digital FIFO: %w", err)
+	}
+
+	err = rdo.dev.hardrocStopDigitalAcquisitionCommand()
+	if err != nil {
+		return xerrors.Errorf("could not stop digital acquisition: %w", err)
+	}
+
+	err = rdo.dev.hardrocFlushDigitalFIFO()
+	if err != nil {
+		return xerrors.Errorf("could not flush digital FIFO: %w", err)
+	}
+
+	return nil
+}
+
+func (rdo *Readout) configureRegisters() error {
+	var err error
+	err = rdo.dev.setDIFID(rdo.difID)
+	if err != nil {
+		return xerrors.Errorf("could not set DIF ID to 0x%x: %w", rdo.difID, err)
+	}
+
+	err = rdo.doRefreshNumASICs()
+	if err != nil {
+		return xerrors.Errorf("could not refresh #ASICs: %w", err)
+	}
+
+	err = rdo.dev.setEventsBetweenTemperatureReadout(5)
+	if err != nil {
+		return xerrors.Errorf("could not set #events Temp readout: %w", err)
+	}
+
+	err = rdo.dev.setAnalogConfigureRegister(0xc0054000)
+	if err != nil {
+		return xerrors.Errorf("could not configure analog register: %w", err)
+	}
+
+	err = rdo.dev.hardrocFlushDigitalFIFO()
+	if err != nil {
+		return xerrors.Errorf("could not flush digital FIFO: %w", err)
+	}
+
+	fw, err := rdo.dev.usbFwVersion()
+	if err != nil {
+		return xerrors.Errorf("could not get firmware version: %w", err)
+	}
+	rdo.msg.Infof("dif %s fw: 0x%x", rdo.name, fw)
+
+	err = rdo.dev.difCptReset()
+	if err != nil {
+		return xerrors.Errorf("could not reset DIF cpt: %w", err)
+	}
+
+	err = rdo.dev.setChipTypeRegister(map[asicKind]uint32{
+		hardrocASIC:  0x100,
+		microrocASIC: 0x1000,
+	}[rdo.asic])
+	if err != nil {
+		return xerrors.Errorf("could not set chip type: %w", err)
+	}
+
+	err = rdo.dev.setControlRegister(rdo.ctlreg)
+	if err != nil {
+		return xerrors.Errorf("could not set control register: %w", err)
+	}
+
+	ctlreg, err := rdo.dev.getControlRegister()
+	if err != nil {
+		return xerrors.Errorf("could not get control register: %w", err)
+	}
+	rdo.msg.Infof("ctl reg: 0x%x", ctlreg)
+
+	err = rdo.dev.setPwr2PwrARegister(rdo.reg.p2pa)
+	if err != nil {
+		return xerrors.Errorf("could not set pwr to A register: %w", err)
+	}
+
+	err = rdo.dev.setPwrA2PwrDRegister(rdo.reg.pa2pd)
+	if err != nil {
+		return xerrors.Errorf("could not set A to D register: %w", err)
+	}
+
+	err = rdo.dev.setPwrD2DAQRegister(rdo.reg.pd2daq)
+	if err != nil {
+		return xerrors.Errorf("could not set D to DAQ register: %w", err)
+	}
+
+	err = rdo.dev.setDAQ2PwrDRegister(rdo.reg.daq2pd)
+	if err != nil {
+		return xerrors.Errorf("could not set DAQ to D register: %w", err)
+	}
+
+	err = rdo.dev.setPwrD2PwrARegister(rdo.reg.pd2pa)
+	if err != nil {
+		return xerrors.Errorf("could not set D to A register: %w", err)
+	}
+
+	return err
+}
+
+func (rdo *Readout) configureChips(scFrame [][]byte) (uint32, error) {
+	var frame []byte
+	switch rdo.asic {
+	case hardrocASIC:
+		frame = make([]byte, hardrocV2SLCFrameSize)
+	case microrocASIC:
+		frame = make([]byte, microrocSLCFrameSize)
+	default:
+		return 0, xerrors.Errorf("unknown ASIC kind %v", rdo.asic)
+	}
+
+	crc := crc16.New(nil)
+	err := rdo.dev.hardrocCmdSLCWrite()
+	if err != nil {
+		return 0, xerrors.Errorf("%s could not send start SLC command to DIF: %w",
+			rdo.name, err,
+		)
+	}
+
+	for i := rdo.nasics; i > 0; i-- {
+		copy(frame, scFrame[i-1])
+		_, err = crc.Write(frame)
+		if err != nil {
+			return 0, xerrors.Errorf("%s could not update CRC-16: %w", rdo.name, err)
+		}
+
+		err = rdo.dev.cmdSLCWriteSingleSLCFrame(frame)
+		if err != nil {
+			return 0, xerrors.Errorf("%s could not send SLC frame to DIF: %w",
+				rdo.name, err,
+			)
+		}
+	}
+
+	crc16 := crc.Sum16()
+	err = rdo.dev.hardrocCmdSLCWriteCRC(crc16)
+	if err != nil {
+		return 0, xerrors.Errorf("%s could not send CRC 0x%x to SLC: %w",
+			rdo.name, crc16, err,
+		)
+	}
+
+	time.Sleep(400 * time.Millisecond) // was 500ms
+
+	st, err := rdo.doReadSLCStatus()
+	if err != nil {
+		return 0, xerrors.Errorf("%s could not read SLC status: %w",
+			rdo.name, err,
+		)
+	}
+	rdo.curSC = st
+
+	return st, nil
+}
+
+func (rdo *Readout) Readout(p []byte) (int, error) {
+	var (
+		dif DIF
+		w   = bwriter{p: p}
+		dec = NewDecoder(uint8(rdo.difID), io.TeeReader(rdo.dev.ft, &w))
+	)
+	err := dec.Decode(&dif)
+	if err != nil {
+		return w.c, xerrors.Errorf("%s could not decode DIF data: %w",
+			rdo.name, err,
+		)
+	}
+
+	return w.c, nil
+}
+
+func (rdo *Readout) doRefreshNumASICs() error {
+	var (
+		v  uint32
+		l1 = uint8(rdo.nasics>>0) & 0xff
+		l2 = uint8(rdo.nasics>>8) & 0xff
+		l3 = uint8(rdo.nasics>>16) & 0xff
+		l4 = uint8(rdo.nasics>>24) & 0xff
+		n  = l1 + l2 + l3 + l4
+	)
+
+	f := func(n, l1, l2, l3, l4 uint8) uint32 {
+		return uint32(n) + uint32(l1<<8) + uint32(l2<<14) + uint32(l3<<20) + uint32(l4<<26)
+	}
+	switch rdo.asic {
+	case microrocASIC:
+		v = f(n, l1, l2, l3, l4)
+	default:
+		v = f(n, n, 0, 0, 0)
+	}
+
+	err := rdo.dev.usbRegWrite(0x05, v)
+	if err != nil {
+		return xerrors.Errorf("could not refresh num-asics: %w", err)
+	}
+
+	return nil
+}
+
+func (rdo *Readout) doReadSLCStatus() (uint32, error) {
+	st, err := rdo.dev.hardrocSLCStatusRead()
+	if err != nil {
+		return 0, xerrors.Errorf("could not read SLC status: %w", err)
+	}
+	rdo.curSC = st
+	return st, nil
+}
+
+type bwriter struct {
+	p []byte
+	c int
+}
+
+func (w *bwriter) Write(p []byte) (int, error) {
+	if w.c >= len(w.p) {
+		return 0, io.EOF
+	}
+	n := copy(w.p[w.c:], p)
+	w.c += n
+	return n, nil
+}
diff --git a/dif/readout_test.go b/dif/readout_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8667fe2d94c90beeff1b4a04d807fda14a1718cc
--- /dev/null
+++ b/dif/readout_test.go
@@ -0,0 +1,148 @@
+// Copyright 2020 The go-lpc Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dif
+
+import (
+	"bytes"
+	"io"
+	"os"
+	"testing"
+
+	"github.com/go-daq/tdaq/log"
+	"github.com/ziutek/ftdi"
+)
+
+func ftdiOpenTest(vid, pid uint16) (ftdiDevice, error) {
+	return &fakeDevice{buf: new(bytes.Buffer)}, nil
+}
+
+func TestReadout(t *testing.T) {
+	ftdiOpen = ftdiOpenTest
+	defer func() {
+		ftdiOpen = ftdiOpenImpl
+	}()
+
+	const (
+		name   = "FT101042"
+		prodID = 0x6014
+	)
+
+	rdo, err := NewReadout(name, prodID, log.NewMsgStream("readout-"+name, log.LvlDebug, os.Stderr))
+	if err != nil {
+		t.Fatalf("could not create readout: %+v", err)
+	}
+
+	err = rdo.configureRegisters()
+	if err != nil {
+		t.Fatalf("could not configure registers: %+v", err)
+	}
+
+	slow := make([][]byte, rdo.nasics)
+	for i := range slow {
+		slow[i] = make([]byte, hardrocV2SLCFrameSize)
+	}
+	_, err = rdo.configureChips(slow)
+	if err != nil {
+		t.Fatalf("could not configure chips: %+v", err)
+	}
+
+	err = rdo.start()
+	if err != nil {
+		t.Fatalf("could not start readout: %+v", err)
+	}
+
+	data := make([]byte, MaxEventSize)
+	{
+		w := new(bytes.Buffer)
+		dif := DIF{
+			Header: GlobalHeader{
+				ID:        uint8(rdo.difID),
+				DTC:       10,
+				ATC:       11,
+				GTC:       12,
+				AbsBCID:   0x0000112233445566,
+				TimeDIFTC: 0x00112233,
+			},
+			Frames: []Frame{
+				{
+					Header: 1,
+					BCID:   0x001a1b1c,
+					Data:   [16]uint8{0xa, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
+				},
+				{
+					Header: 2,
+					BCID:   0x002a2b2c,
+					Data: [16]uint8{
+						0xb, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+						210, 211, 212, 213, 214, 215,
+					},
+				},
+			},
+		}
+		err = NewEncoder(w).Encode(&dif)
+		if err != nil {
+			t.Fatalf("could not encode DIF data: %+v", err)
+		}
+		rdo.dev.ft = &fakeDevice{w}
+	}
+	n, err := rdo.Readout(data)
+	if err != nil {
+		t.Fatalf("could not readout data: %+v", err)
+	}
+	if n <= 0 {
+		t.Fatalf("could not readout data: n=%d", n)
+	}
+	data = data[:n]
+
+	err = rdo.stop()
+	if err != nil {
+		t.Fatalf("could not stop readout: %+v", err)
+	}
+
+	err = rdo.close()
+	if err != nil {
+		t.Fatalf("could not close readout: %+v", err)
+	}
+}
+
+type fakeDevice struct {
+	buf io.ReadWriter
+}
+
+func (dev *fakeDevice) Reset() error { return nil }
+
+func (dev *fakeDevice) SetBitmode(iomask byte, mode ftdi.Mode) error {
+	return nil
+}
+
+func (dev *fakeDevice) SetFlowControl(flowctrl ftdi.FlowCtrl) error {
+	return nil
+}
+
+func (dev *fakeDevice) SetLatencyTimer(lt int) error {
+	return nil
+}
+
+func (dev *fakeDevice) SetWriteChunkSize(cs int) error {
+	return nil
+}
+
+func (dev *fakeDevice) SetReadChunkSize(cs int) error {
+	return nil
+}
+
+func (dev *fakeDevice) PurgeBuffers() error {
+	return nil
+}
+
+func (dev *fakeDevice) Read(p []byte) (int, error) {
+	return dev.buf.Read(p)
+}
+
+func (dev *fakeDevice) Write(p []byte) (int, error) {
+	return dev.buf.Write(p)
+}
+
+func (dev *fakeDevice) Close() error { return nil }
diff --git a/go.mod b/go.mod
index 89756fd475824adce515bf937e8165cc4e9b6d70..899edc2bee30beb553189ba90fc05ff02c7abebb 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.14
 require (
 	github.com/go-daq/tdaq v0.13.0
 	github.com/peterh/liner v1.1.0
+	github.com/ziutek/ftdi v0.0.0-20181130113013-aef9e445a2fa
 	golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
 	golang.org/x/sys v0.0.0-20200217220822-9197077df867 // indirect
 	golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
diff --git a/go.sum b/go.sum
index 426ec820725fb490ddaf3fcd0ba29c0e7cbc0e7e..af8130451a5f7c9d5079dd5aaa650f3cfb18a7f3 100644
--- a/go.sum
+++ b/go.sum
@@ -26,6 +26,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qq
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/ziutek/ftdi v0.0.0-20181130113013-aef9e445a2fa h1:8J6YeSHVtoJG5sodX1Y6qPM3eaLmxC0noolyHwWdbsM=
+github.com/ziutek/ftdi v0.0.0-20181130113013-aef9e445a2fa/go.mod h1:N08Z75zCaXPhywWVtQxjPLGCh0HtZ0pUIEeV/UlHmhI=
 go.nanomsg.org/mangos/v3 v3.0.0 h1:50i1v/XDhTcYW2CnJYxjpuHKf041obWL0Ccezz6SsJs=
 go.nanomsg.org/mangos/v3 v3.0.0/go.mod h1:+o2RKKCIEB6Dw/4Frf2biiEuqcRDKcEUSrC0r9OWDE0=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -46,7 +48,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -59,7 +60,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
 golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200217220822-9197077df867 h1:JoRuNIf+rpHl+VhScRQQvzbHed86tKkqwPMV34T8myw=
 golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=