diff --git a/dif/decoder.go b/dif/decoder.go
index fb28f8e302075f136cff230d9abdf16f9930a2cf..7fc2998fb1b1bdab74c08818735ebb1765642113 100644
--- a/dif/decoder.go
+++ b/dif/decoder.go
@@ -95,7 +95,7 @@ func (dec *Decoder) Decode(dif *DIF) error {
 	dif.Header.DTC = binary.BigEndian.Uint32(hdr[1 : 1+4])
 	dif.Header.ATC = binary.BigEndian.Uint32(hdr[5 : 5+4])
 	dif.Header.GTC = binary.BigEndian.Uint32(hdr[9 : 9+4])
-	copy(dif.Header.AbsBCID[:], hdr[13:15+4])
+	copy(dif.Header.AbsBCID[:], hdr[13:13+6])
 	copy(dif.Header.TimeDIFTC[:], hdr[19:19+3])
 	dif.Frames = dif.Frames[:0]
 
@@ -203,8 +203,9 @@ func (dec *Decoder) readU8() uint8 {
 }
 
 func (dec *Decoder) readU16() uint16 {
-	dec.load(2)
-	return binary.BigEndian.Uint16(dec.buf[:2])
+	const n = 2
+	dec.load(n)
+	return binary.BigEndian.Uint16(dec.buf[:n])
 }
 
 func (dec *Decoder) load(n int) {
diff --git a/dif/encoder.go b/dif/encoder.go
new file mode 100644
index 0000000000000000000000000000000000000000..322dce9821d585d944f778fe8f1661118b1405fe
--- /dev/null
+++ b/dif/encoder.go
@@ -0,0 +1,105 @@
+// 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/go-lpc/mim/internal/crc16"
+	"golang.org/x/xerrors"
+)
+
+type Encoder struct {
+	w   io.Writer
+	buf []byte
+	err error
+	crc crc16.Hash16
+}
+
+func NewEncoder(w io.Writer) *Encoder {
+	return &Encoder{
+		w:   w,
+		buf: make([]byte, 8),
+		crc: crc16.New(nil),
+	}
+}
+
+func (enc *Encoder) crcw(p []byte) {
+	_, _ = enc.crc.Write(p) // can not fail.
+}
+
+func (enc *Encoder) reset() {
+	enc.crc.Reset()
+}
+
+func (enc *Encoder) Encode(dif *DIF) error {
+	if dif == nil {
+		return nil
+	}
+
+	enc.reset()
+
+	enc.writeU8(gbHeader)
+	if enc.err != nil {
+		return xerrors.Errorf("dif: could not write global header marker: %w", enc.err)
+	}
+
+	enc.writeU8(dif.Header.ID)
+	enc.writeU32(dif.Header.DTC)
+	enc.writeU32(dif.Header.ATC)
+	enc.writeU32(dif.Header.GTC)
+	enc.write(dif.Header.AbsBCID[:])
+	enc.write(dif.Header.TimeDIFTC[:])
+	enc.writeU8(0) // nlines
+
+	enc.writeU8(frHeader)
+	for _, frame := range dif.Frames {
+		enc.writeU8(frame.Header)
+		enc.write(frame.BCID[:])
+		enc.write(frame.Data[:])
+	}
+	enc.writeU8(frTrailer)
+	crc := enc.crc.Sum16() // gb-trailer not part of CRC-16
+	enc.writeU8(gbTrailer)
+	enc.writeU16(crc)
+
+	return enc.err
+}
+
+func (enc *Encoder) write(p []byte) {
+	if enc.err != nil {
+		return
+	}
+	_, enc.err = enc.w.Write(p)
+	enc.crcw(p)
+}
+
+func (enc *Encoder) writeU8(v uint8) {
+	const n = 1
+	enc.reserve(n)
+	enc.buf[0] = v
+	enc.write(enc.buf[:n])
+}
+
+func (enc *Encoder) writeU16(v uint16) {
+	const n = 2
+	enc.reserve(n)
+	binary.BigEndian.PutUint16(enc.buf[:n], v)
+	enc.write(enc.buf[:n])
+}
+
+func (enc *Encoder) writeU32(v uint32) {
+	const n = 4
+	enc.reserve(n)
+	binary.BigEndian.PutUint32(enc.buf[:n], v)
+	enc.write(enc.buf[:n])
+}
+
+func (enc *Encoder) reserve(n int) {
+	if cap(enc.buf) < n {
+		enc.buf = append(enc.buf[:len(enc.buf)], make([]byte, n-cap(enc.buf))...)
+	}
+}
diff --git a/dif/decoder_test.go b/dif/rw_test.go
similarity index 82%
rename from dif/decoder_test.go
rename to dif/rw_test.go
index ba568944b145aa6af6c5563ec168ad70619c29b8..257361ed0540c338c381b2fa0c90d109c72a2f39 100644
--- a/dif/decoder_test.go
+++ b/dif/rw_test.go
@@ -7,11 +7,117 @@ package dif
 import (
 	"bytes"
 	"io"
+	"reflect"
 	"testing"
 
 	"golang.org/x/xerrors"
 )
 
+func TestCodec(t *testing.T) {
+	const (
+		difID = 0x42
+	)
+
+	for _, tc := range []struct {
+		name string
+		dif  DIF
+	}{
+		{
+			name: "normal",
+			dif: DIF{
+				Header: GlobalHeader{
+					ID:        difID,
+					DTC:       10,
+					ATC:       11,
+					GTC:       12,
+					AbsBCID:   [6]uint8{1, 2, 3, 4, 5, 6},
+					TimeDIFTC: [3]uint8{10, 11, 12},
+				},
+				Frames: []Frame{
+					{
+						Header: 1,
+						BCID:   [3]uint8{10, 11, 12},
+						Data:   [16]uint8{0xa, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
+					},
+					{
+						Header: 2,
+						BCID:   [3]uint8{20, 21, 22},
+						Data: [16]uint8{
+							0xb, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+							210, 211, 212, 213, 214, 215,
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "no-frame",
+			dif: DIF{
+				Header: GlobalHeader{
+					ID:        difID,
+					DTC:       10,
+					ATC:       11,
+					GTC:       12,
+					AbsBCID:   [6]uint8{1, 2, 3, 4, 5, 6},
+					TimeDIFTC: [3]uint8{10, 11, 12},
+				},
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			buf := new(bytes.Buffer)
+			enc := NewEncoder(buf)
+			err := enc.Encode(&tc.dif)
+			if err != nil {
+				t.Fatalf("could not encode dif frames: %+v", err)
+			}
+
+			dec := NewDecoder(difID, buf)
+			var got DIF
+			err = dec.Decode(&got)
+			if err != nil {
+				t.Fatalf("could not decode dif frames: %+v", err)
+			}
+
+			if got, want := got, tc.dif; !reflect.DeepEqual(got, want) {
+				t.Fatalf("invalid r/w round-trip:\ngot= %#v\nwant=%#v", got, want)
+			}
+		})
+	}
+}
+
+func TestEncoder(t *testing.T) {
+	{
+		buf := new(bytes.Buffer)
+		enc := NewEncoder(buf)
+
+		if got, want := enc.Encode(nil), error(nil); got != want {
+			t.Fatalf("invalid nil-dif encoding: got=%v, want=%v", got, want)
+		}
+	}
+	{
+		buf := failingWriter{n: 0}
+		enc := NewEncoder(&buf)
+		if got, want := enc.Encode(&DIF{}), xerrors.Errorf("dif: could not write global header marker: %w", io.ErrUnexpectedEOF); got.Error() != want.Error() {
+			t.Fatalf("invalid error:\ngot= %+v\nwant=%+v", got, want)
+		}
+	}
+
+}
+
+type failingWriter struct {
+	n   int
+	cur int
+}
+
+func (w *failingWriter) Write(p []byte) (int, error) {
+	w.cur += len(p)
+	if w.cur >= w.n {
+		return 0, io.ErrUnexpectedEOF
+	}
+	return len(p), nil
+}
+
 func TestDecoder(t *testing.T) {
 	const (
 		difID = 0x42