Add initial J-Link SPI programmer

Tested with SEGGER J-Link EDU, Flasher ARM and flash chip W25Q16.V.

Change-Id: Ie03a054a75457ec9e1cab36ea124bb53b10e8d7e
Signed-off-by: Marc Schink <flashrom-dev@marcschink.de>
Reviewed-on: https://review.coreboot.org/c/28087
Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
Reviewed-by: Nico Huber <nico.h@gmx.de>
diff --git a/Makefile b/Makefile
index 1ff578c..1e7db3e 100644
--- a/Makefile
+++ b/Makefile
@@ -656,6 +656,9 @@
 # Digilent Development board JTAG
 CONFIG_DIGILENT_SPI ?= yes
 
+# Disable J-Link for now.
+CONFIG_JLINK_SPI ?= no
+
 # Disable wiki printing by default. It is only useful if you have wiki access.
 CONFIG_PRINT_WIKI ?= no
 
@@ -964,6 +967,12 @@
 NEED_LIBUSB1 += CONFIG_DIGILENT_SPI
 endif
 
+ifeq ($(CONFIG_JLINK_SPI), yes)
+NEED_LIBJAYLINK += CONFIG_JLINK_SPI
+FEATURE_CFLAGS += -D'CONFIG_JLINK_SPI=1'
+PROGRAMMER_OBJS += jlink_spi.o
+endif
+
 ifneq ($(NEED_SERIAL), )
 LIB_OBJS += serial.o custom_baud.o
 endif
@@ -1038,6 +1047,12 @@
 endif
 endif
 
+ifneq ($(NEED_LIBJAYLINK), )
+CHECK_LIBJAYLINK = yes
+JAYLINKLIBS += $(call debug_shell,[ -n "$(PKG_CONFIG_LIBDIR)" ] && export PKG_CONFIG_LIBDIR="$(PKG_CONFIG_LIBDIR)"; $(PKG_CONFIG) --libs libjaylink)
+override CPPFLAGS += $(call debug_shell,[ -n "$(PKG_CONFIG_LIBDIR)" ] && export PKG_CONFIG_LIBDIR="$(PKG_CONFIG_LIBDIR)"; $(PKG_CONFIG) --cflags-only-I libjaylink)
+endif
+
 ifeq ($(CONFIG_PRINT_WIKI), yes)
 FEATURE_CFLAGS += -D'CONFIG_PRINT_WIKI=1'
 CLI_OBJS += print_wiki.o
@@ -1060,7 +1075,7 @@
 endif
 
 $(PROGRAM)$(EXEC_SUFFIX): $(OBJS)
-	$(CC) $(LDFLAGS) -o $(PROGRAM)$(EXEC_SUFFIX) $(OBJS) $(LIBS) $(PCILIBS) $(FEATURE_LIBS) $(USBLIBS) $(USB1LIBS)
+	$(CC) $(LDFLAGS) -o $(PROGRAM)$(EXEC_SUFFIX) $(OBJS) $(LIBS) $(PCILIBS) $(FEATURE_LIBS) $(USBLIBS) $(USB1LIBS) $(JAYLINKLIBS)
 
 libflashrom.a: $(LIBFLASHROM_OBJS)
 	$(AR) rcs $@ $^
@@ -1194,6 +1209,24 @@
 endef
 export LIBUSB1_TEST
 
+define LIBJAYLINK_TEST
+#include <stddef.h>
+#include <libjaylink/libjaylink.h>
+int main(int argc, char **argv)
+{
+	struct jaylink_context *ctx;
+
+	(void)argc;
+	(void)argv;
+
+	jaylink_init(&ctx);
+	jaylink_exit(ctx);
+
+	return 0;
+}
+endef
+export LIBJAYLINK_TEST
+
 hwlibs: compiler
 	@printf "" > .libdeps
 ifeq ($(CHECK_LIBPCI), yes)
@@ -1272,6 +1305,28 @@
 		rm -f .test.c .test.o .test$(EXEC_SUFFIX); exit 1; }; } 2>>$(BUILD_DETAILS_FILE); echo $? >&3 ; } | tee -a $(BUILD_DETAILS_FILE) >&4; } 3>&1;} | { read rc ; exit ${rc}; } } 4>&1
 	@rm -f .test.c .test.o .test$(EXEC_SUFFIX)
 endif
+ifeq ($(CHECK_LIBJAYLINK), yes)
+	@printf "Checking for libjaylink headers... " | tee -a $(BUILD_DETAILS_FILE)
+	@echo "$$LIBJAYLINK_TEST" > .test.c
+	@printf "\nexec: %s\n" "$(CC) -c $(CPPFLAGS) $(CFLAGS) .test.c -o .test.o" >>$(BUILD_DETAILS_FILE)
+	@{ { { { { $(CC) -c $(CPPFLAGS) $(CFLAGS) .test.c -o .test.o >&2 && \
+		echo "found." || { echo "not found."; echo;				\
+		echo "The following feature requires libjaylink: $(NEED_LIBJAYLINK).";	\
+		echo "Please install libjaylink headers or disable the feature"; \
+		echo "mentioned above by specifying make CONFIG_JLINK_SPI=no"; \
+		echo "See README for more information."; echo;				\
+		rm -f .test.c .test.o; exit 1; }; } 2>>$(BUILD_DETAILS_FILE); echo $? >&3 ; } | tee -a $(BUILD_DETAILS_FILE) >&4; } 3>&1;} | { read rc ; exit ${rc}; } } 4>&1
+	@printf "Checking if libjaylink is usable... " | tee -a $(BUILD_DETAILS_FILE)
+	@printf "\nexec: %s\n" "$(CC) $(LDFLAGS) .test.o -o .test$(EXEC_SUFFIX) $(LIBS) $(JAYLINKLIBS)" >>$(BUILD_DETAILS_FILE)
+	@{ { { { { $(CC) $(LDFLAGS) .test.o -o .test$(EXEC_SUFFIX) $(LIBS) $(JAYLINKLIBS) >&2 && \
+		echo "yes." || { echo "no.";						\
+		echo "The following feature requires libjaylink: $(NEED_LIBJAYLINK).";	\
+		echo "Please install libjaylink or disable the feature"; \
+		echo "mentioned above by specifying make CONFIG_JLINK_SPI=no"; \
+		echo "See README for more information."; echo;				\
+		rm -f .test.c .test.o .test$(EXEC_SUFFIX); exit 1; }; } 2>>$(BUILD_DETAILS_FILE); echo $? >&3 ; } | tee -a $(BUILD_DETAILS_FILE) >&4; } 3>&1;} | { read rc ; exit ${rc}; } } 4>&1
+	@rm -f .test.c .test.o .test$(EXEC_SUFFIX)
+endif
 
 .features: features
 
diff --git a/README b/README
index b5ed946..4e7bd4f 100644
--- a/README
+++ b/README
@@ -50,6 +50,7 @@
  * pciutils+libpci (if you want support for mainboard or PCI device flashing)
  * libusb (if you want FT2232, Dediprog or USB-Blaster support)
  * libftdi (if you want FT2232 or USB-Blaster support)
+ * libjaylink (if you want support for SEGGER J-Link and compatible devices)
 
 Linux et al:
 
diff --git a/flashrom.8.tmpl b/flashrom.8.tmpl
index c557af7..c1a228b 100644
--- a/flashrom.8.tmpl
+++ b/flashrom.8.tmpl
@@ -331,6 +331,8 @@
 .sp
 .BR "* digilent_spi" " (for SPI flash ROMs attached to iCEblink40 development boards)"
 .sp
+.BR "* jlink_spi" " (for SPI flash ROMs attached to SEGGER J-Link and compatible devices)"
+.sp
 Some programmers have optional or mandatory parameters which are described
 in detail in the
 .B PROGRAMMER-SPECIFIC INFORMATION
@@ -1131,6 +1133,60 @@
 (in Hz). The default is a frequency of 4 MHz.
 .sp
 .SS
+.BR "jlink_spi " programmer
+.IP
+This module supports SEGGER J-Link and compatible devices.
+
+The \fBMOSI\fP signal of the flash chip must be attached to \fBTDI\fP pin of
+the programmer, \fBMISO\fP to \fBTDO\fP and \fBSCK\fP to \fBTCK\fP.
+The chip select (\fBCS\fP) signal of the flash chip can be attached to
+different pins of the programmer which can be selected with the
+.sp
+.B "  flashrom \-p jlink_spi:cs=pin"
+.sp
+syntax where \fBpin\fP can be either \fBTRST\fP or \fBRESET\fP.
+The default pin for chip select is \fBRESET\fP.
+Note that, when using \fBRESET\fP, it is normal that the indicator LED blinks
+orange or red.
+.br
+Additionally, the \fBVTref\fP pin of the programmer must be attached to the
+logic level of the flash chip.
+The programmer measures the voltage on this pin and generates the reference
+voltage for its input comparators and adapts its output voltages to it.
+.sp
+Pinout for devices with 20-pin JTAG connector:
+.sp
+    +-------+
+    |  1  2 |     1: VTref     2:
+    |  3  4 |     3: TRST      4: GND
+    |  5  6 |     5: TDI       6: GND
+  +-+  7  8 |     7:           8: GND
+  |    9 10 |     9: TCK      10: GND
+  |   11 12 |    11:          12: GND
+  +-+ 13 14 |    13: TDO      14:
+    | 15 16 |    15: RESET    16:
+    | 17 18 |    17:          18:
+    | 19 20 |    19: PWR_5V   20:
+    +-------+
+.sp
+If there is more than one compatible device connected, you can select which one
+should be used by specifying its serial number with the
+.sp
+.B "  flashrom \-p jlink_spi:serial=number"
+.sp
+syntax where
+.B number
+is the serial number of the device (which can be found for example in the
+output of lsusb -v).
+.sp
+The SPI speed can be selected by using the
+.sp
+.B "  flashrom \-p jlink_spi:spispeed=frequency"
+.sp
+syntax where \fBfrequency\fP is the SPI clock frequency in kHz.
+The maximum speed depends on the device in use.
+.SS
+
 .SH EXAMPLES
 To back up and update your BIOS, run
 .sp
diff --git a/flashrom.c b/flashrom.c
index 59a7531..7129a51 100644
--- a/flashrom.c
+++ b/flashrom.c
@@ -437,6 +437,18 @@
 	},
 #endif
 
+#if CONFIG_JLINK_SPI == 1
+	{
+		.name			= "jlink_spi",
+		.type			= OTHER,
+		.init			= jlink_spi_init,
+		.devs.note		= "SEGGER J-Link and compatible devices\n",
+		.map_flash_region	= fallback_map,
+		.unmap_flash_region	= fallback_unmap,
+		.delay			= internal_delay,
+	},
+#endif
+
 	{0}, /* This entry corresponds to PROGRAMMER_INVALID. */
 };
 
diff --git a/jlink_spi.c b/jlink_spi.c
new file mode 100644
index 0000000..08a9ba9
--- /dev/null
+++ b/jlink_spi.c
@@ -0,0 +1,456 @@
+/*
+ * This file is part of the flashrom project.
+ *
+ * Copyright (C) 2016 Marc Schink <flashrom-dev@marcschink.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+/*
+ * Driver for the J-Link hardware by SEGGER.
+ * See https://www.segger.com/ for more info.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <libjaylink/libjaylink.h>
+
+#include "flash.h"
+#include "programmer.h"
+#include "spi.h"
+
+/*
+ * Maximum number of bytes that can be transferred at once via the JTAG
+ * interface, see jaylink_jtag_io().
+ */
+#define JTAG_MAX_TRANSFER_SIZE	(UINT16_MAX / 8)
+
+/*
+ * Default base frequency in Hz. Used when the base frequency can not be
+ * retrieved from the device.
+ */
+#define DEFAULT_FREQ		16000000
+
+/*
+ * Default frequency divider. Used when the frequency divider can not be
+ * retrieved from the device.
+ */
+#define DEFAULT_FREQ_DIV	4
+
+/* Minimum target voltage required for operation in mV. */
+#define MIN_TARGET_VOLTAGE	1200
+
+static struct jaylink_context *jaylink_ctx;
+static struct jaylink_device_handle *jaylink_devh;
+static bool reset_cs;
+
+static bool assert_cs(void)
+{
+	int ret;
+
+	if (reset_cs) {
+		ret = jaylink_clear_reset(jaylink_devh);
+
+		if (ret != JAYLINK_OK) {
+			msg_perr("jaylink_clear_reset() failed: %s.\n", jaylink_strerror(ret));
+			return false;
+		}
+	} else {
+		ret = jaylink_jtag_clear_trst(jaylink_devh);
+
+		if (ret != JAYLINK_OK) {
+			msg_perr("jaylink_jtag_clear_trst() failed: %s.\n", jaylink_strerror(ret));
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static bool deassert_cs(void)
+{
+	int ret;
+
+	if (reset_cs) {
+		ret = jaylink_set_reset(jaylink_devh);
+
+		if (ret != JAYLINK_OK) {
+			msg_perr("jaylink_set_reset() failed: %s.\n", jaylink_strerror(ret));
+			return false;
+		}
+	} else {
+		ret = jaylink_jtag_set_trst(jaylink_devh);
+
+		if (ret != JAYLINK_OK) {
+			msg_perr("jaylink_jtag_set_trst() failed: %s.\n", jaylink_strerror(ret));
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static int jlink_spi_send_command(struct flashctx *flash, unsigned int writecnt, unsigned int readcnt,
+		const unsigned char *writearr, unsigned char *readarr)
+{
+	uint32_t length;
+	uint8_t *buffer;
+
+	length = writecnt + readcnt;
+
+	if (length > JTAG_MAX_TRANSFER_SIZE)
+		return SPI_INVALID_LENGTH;
+
+	buffer = malloc(length);
+
+	if (!buffer) {
+		msg_perr("Memory allocation failed.\n");
+		return SPI_GENERIC_ERROR;
+	}
+
+	/* Reverse all bytes because the device transfers data LSB first. */
+	reverse_bytes(buffer, writearr, writecnt);
+
+	memset(buffer + writecnt, 0x00, readcnt);
+
+	if (!assert_cs()) {
+		free(buffer);
+		return SPI_PROGRAMMER_ERROR;
+	}
+
+	int ret;
+
+	ret = jaylink_jtag_io(jaylink_devh, buffer, buffer, buffer, length * 8, JAYLINK_JTAG_VERSION_2);
+
+	if (ret != JAYLINK_OK) {
+		msg_perr("jaylink_jag_io() failed: %s.\n", jaylink_strerror(ret));
+		free(buffer);
+		return SPI_PROGRAMMER_ERROR;
+	}
+
+	if (!deassert_cs()) {
+		free(buffer);
+		return SPI_PROGRAMMER_ERROR;
+	}
+
+	/* Reverse all bytes because the device transfers data LSB first. */
+	reverse_bytes(readarr, buffer + writecnt, readcnt);
+	free(buffer);
+
+	return 0;
+}
+
+static const struct spi_master spi_master_jlink_spi = {
+	.type		= SPI_CONTROLLER_JLINK_SPI,
+	/* Maximum data read size in one go (excluding opcode+address). */
+	.max_data_read	= JTAG_MAX_TRANSFER_SIZE - 5,
+	/* Maximum data write size in one go (excluding opcode+address). */
+	.max_data_write	= JTAG_MAX_TRANSFER_SIZE - 5,
+	.command	= jlink_spi_send_command,
+	.multicommand	= default_spi_send_multicommand,
+	.read		= default_spi_read,
+	.write_256	= default_spi_write_256,
+	.write_aai	= default_spi_write_aai,
+	.features	= SPI_MASTER_4BA,
+};
+
+static int jlink_spi_shutdown(void *data)
+{
+	if (jaylink_devh)
+		jaylink_close(jaylink_devh);
+
+	jaylink_exit(jaylink_ctx);
+
+	return 0;
+}
+
+int jlink_spi_init(void)
+{
+	char *arg;
+	unsigned long speed = 0;
+
+	register_shutdown(jlink_spi_shutdown, NULL);
+
+	arg = extract_programmer_param("spispeed");
+
+	if (arg) {
+		char *endptr;
+
+		errno = 0;
+		speed = strtoul(arg, &endptr, 10);
+
+		if (*endptr != '\0' || errno != 0) {
+			msg_perr("Invalid SPI speed specified: %s.\n", arg);
+			free(arg);
+			return 1;
+		}
+
+		if (speed < 1) {
+			msg_perr("SPI speed must be at least 1 kHz.\n");
+			free(arg);
+			return 1;
+		}
+	}
+
+	free(arg);
+
+	int ret;
+	bool use_serial_number;
+	uint32_t serial_number;
+
+	arg = extract_programmer_param("serial");
+
+	if (arg) {
+		if (!strlen(arg)) {
+			msg_perr("Emptpy serial number specified.\n");
+			free(arg);
+			return 1;
+		}
+
+		ret = jaylink_parse_serial_number(arg, &serial_number);
+
+		if (ret == JAYLINK_ERR) {
+			msg_perr("Invalid serial number specified: %s.\n", arg);
+			free(arg);
+			return 1;
+		} if (ret != JAYLINK_OK) {
+			msg_perr("jaylink_parse_serial_number() failed: %s.\n", jaylink_strerror(ret));
+			free(arg);
+			return 1;
+		}
+
+		use_serial_number = true;
+	} else {
+		use_serial_number = false;
+	}
+
+	free(arg);
+
+	reset_cs = true;
+	arg = extract_programmer_param("cs");
+
+	if (arg) {
+		if (!strcasecmp(arg, "reset")) {
+			reset_cs = true;
+		} else if (!strcasecmp(arg, "trst")) {
+			reset_cs = false;
+		} else {
+			msg_perr("Invalid chip select pin specified: '%s'.\n", arg);
+			free(arg);
+			return 1;
+		}
+	}
+
+	free(arg);
+
+	if (reset_cs)
+		msg_pdbg("Using RESET as chip select signal.\n");
+	else
+		msg_pdbg("Using TRST as chip select signal.\n");
+
+	ret = jaylink_init(&jaylink_ctx);
+
+	if (ret != JAYLINK_OK) {
+		msg_perr("jaylink_init() failed: %s.\n", jaylink_strerror(ret));
+		return 1;
+	}
+
+	ret = jaylink_discovery_scan(jaylink_ctx, 0);
+
+	if (ret != JAYLINK_OK) {
+		msg_perr("jaylink_discover_scan() failed: %s.\n", jaylink_strerror(ret));
+		return 1;
+	}
+
+	struct jaylink_device **devs;
+
+	ret = jaylink_get_devices(jaylink_ctx, &devs, NULL);
+
+	if (ret != JAYLINK_OK) {
+		msg_perr("jaylink_get_devices() failed: %s.\n", jaylink_strerror(ret));
+		return 1;
+	}
+
+	if (!use_serial_number)
+		msg_pdbg("No device selected, using first device.\n");
+
+	size_t i;
+	struct jaylink_device *dev;
+	bool device_found = false;
+
+	for (i = 0; devs[i]; i++) {
+		if (use_serial_number) {
+			uint32_t tmp;
+
+			ret = jaylink_device_get_serial_number(devs[i], &tmp);
+
+			if (ret == JAYLINK_ERR_NOT_AVAILABLE) {
+				continue;
+			} else if (ret != JAYLINK_OK) {
+				msg_pwarn("jaylink_device_get_serial_number() failed: %s.\n",
+					jaylink_strerror(ret));
+				continue;
+			}
+
+			if (serial_number != tmp)
+				continue;
+		}
+
+		ret = jaylink_open(devs[i], &jaylink_devh);
+
+		if (ret == JAYLINK_OK) {
+			dev = devs[i];
+			device_found = true;
+			break;
+		}
+
+		jaylink_devh = NULL;
+	}
+
+	jaylink_free_devices(devs, true);
+
+	if (!device_found) {
+		msg_perr("No J-Link device found.\n");
+		return 1;
+	}
+
+	size_t length;
+	char *firmware_version;
+
+	ret = jaylink_get_firmware_version(jaylink_devh, &firmware_version,
+		&length);
+
+	if (ret != JAYLINK_OK) {
+		msg_perr("jaylink_get_firmware_version() failed: %s.\n", jaylink_strerror(ret));
+		return 1;
+	} else if (length > 0) {
+		msg_pdbg("Firmware: %s\n", firmware_version);
+		free(firmware_version);
+	}
+
+	ret = jaylink_device_get_serial_number(dev, &serial_number);
+
+	if (ret == JAYLINK_OK) {
+		msg_pdbg("S/N: %" PRIu32 "\n", serial_number);
+	} else if (ret == JAYLINK_ERR_NOT_AVAILABLE) {
+		msg_pdbg("S/N: N/A\n");
+	} else {
+		msg_perr("jaylink_device_get_serial_number() failed: %s.\n", jaylink_strerror(ret));
+		return 1;
+	}
+
+	uint8_t caps[JAYLINK_DEV_EXT_CAPS_SIZE];
+
+	memset(caps, 0, sizeof(caps));
+	ret = jaylink_get_caps(jaylink_devh, caps);
+
+	if (ret != JAYLINK_OK) {
+		msg_perr("jaylink_get_caps() failed: %s.\n", jaylink_strerror(ret));
+		return 1;
+	}
+
+	if (jaylink_has_cap(caps, JAYLINK_DEV_CAP_GET_EXT_CAPS)) {
+		ret = jaylink_get_extended_caps(jaylink_devh, caps);
+
+		if (ret != JAYLINK_OK) {
+			msg_perr("jaylink_get_available_interfaces() failed: %s.\n", jaylink_strerror(ret));
+			return 1;
+		}
+	}
+
+	uint32_t ifaces;
+
+	ret = jaylink_get_available_interfaces(jaylink_devh, &ifaces);
+
+	if (ret != JAYLINK_OK) {
+		msg_perr("jaylink_get_available_interfaces() failed: %s.\n", jaylink_strerror(ret));
+		return 1;
+	}
+
+	if (!(ifaces & (1 << JAYLINK_TIF_JTAG))) {
+		msg_perr("Device does not support JTAG interface.\n");
+		return 1;
+	}
+
+	ret = jaylink_select_interface(jaylink_devh, JAYLINK_TIF_JTAG, NULL);
+
+	if (ret != JAYLINK_OK) {
+		msg_perr("jaylink_select_interface() failed: %s.\n", jaylink_strerror(ret));
+		return 1;
+	}
+
+	struct jaylink_hardware_status hwstat;
+
+	ret = jaylink_get_hardware_status(jaylink_devh, &hwstat);
+
+	if (ret != JAYLINK_OK) {
+		msg_perr("jaylink_get_hardware_status() failed: %s.\n", jaylink_strerror(ret));
+		return 1;
+	}
+
+	msg_pdbg("VTarget: %u.%03u V\n", hwstat.target_voltage / 1000,
+		hwstat.target_voltage % 1000);
+
+	if (hwstat.target_voltage < MIN_TARGET_VOLTAGE) {
+		msg_perr("Target voltage is below %u.%03u V. You need to attach VTref to the I/O voltage of "
+			"the chip.\n", MIN_TARGET_VOLTAGE / 1000, MIN_TARGET_VOLTAGE % 1000);
+		return 1;
+	}
+
+	struct jaylink_speed device_speeds;
+
+	device_speeds.freq = DEFAULT_FREQ;
+	device_speeds.div = DEFAULT_FREQ_DIV;
+
+	if (jaylink_has_cap(caps, JAYLINK_DEV_CAP_GET_SPEEDS)) {
+		ret = jaylink_get_speeds(jaylink_devh, &device_speeds);
+
+		if (ret != JAYLINK_OK) {
+			msg_perr("jaylink_get_speeds() failed: %s.\n", jaylink_strerror(ret));
+			return 1;
+		}
+	}
+
+	device_speeds.freq /= 1000;
+
+	msg_pdbg("Maximum SPI speed: %" PRIu32 " kHz\n", device_speeds.freq / device_speeds.div);
+
+	if (!speed) {
+		speed = device_speeds.freq / device_speeds.div;
+		msg_pdbg("SPI speed not specified, using %lu kHz.\n", speed);
+	}
+
+	if (speed > (device_speeds.freq / device_speeds.div)) {
+		msg_perr("Specified SPI speed of %lu kHz is too high. Maximum is %" PRIu32 " kHz.\n", speed,
+			device_speeds.freq / device_speeds.div);
+		return 1;
+	}
+
+	ret = jaylink_set_speed(jaylink_devh, speed);
+
+	if (ret != JAYLINK_OK) {
+		msg_perr("jaylink_set_speed() failed: %s.\n", jaylink_strerror(ret));
+		return 1;
+	}
+
+	msg_pdbg("SPI speed: %lu kHz\n", speed);
+
+	/* Ensure that the CS signal is not active initially. */
+	if (!deassert_cs())
+		return 1;
+
+	register_spi_master(&spi_master_jlink_spi);
+
+	return 0;
+}
diff --git a/programmer.h b/programmer.h
index 311992a..8bf7d29 100644
--- a/programmer.h
+++ b/programmer.h
@@ -118,6 +118,9 @@
 #if CONFIG_DIGILENT_SPI == 1
 	PROGRAMMER_DIGILENT_SPI,
 #endif
+#if CONFIG_JLINK_SPI == 1
+	PROGRAMMER_JLINK_SPI,
+#endif
 	PROGRAMMER_INVALID /* This must always be the last entry. */
 };
 
@@ -573,6 +576,11 @@
 extern const struct dev_entry devs_digilent_spi[];
 #endif
 
+/* jlink_spi.c */
+#if CONFIG_JLINK_SPI == 1
+int jlink_spi_init(void);
+#endif
+
 /* flashrom.c */
 struct decode_sizes {
 	uint32_t parallel;
@@ -641,6 +649,9 @@
 #if CONFIG_DIGILENT_SPI == 1
 	SPI_CONTROLLER_DIGILENT_SPI,
 #endif
+#if CONFIG_JLINK_SPI == 1
+	SPI_CONTROLLER_JLINK_SPI,
+#endif
 };
 
 #define MAX_DATA_UNSPECIFIED 0