Browse Source

Merge branch 'drillbit' into bfgminer

Luke Dashjr 12 years ago
parent
commit
22a9405474
4 changed files with 741 additions and 0 deletions
  1. 1 0
      70-bfgminer.rules.in
  2. 4 0
      Makefile.am
  3. 25 0
      configure.ac
  4. 711 0
      driver-drillbit.c

+ 1 - 0
70-bfgminer.rules.in

@@ -7,6 +7,7 @@ LABEL="bfgminer_start"
 @USE_BIFURY_TRUE@ENV{ID_MODEL}=="*bi•fury*", GOTO="bfgminer_add"
 @HAS_BIGPIC_TRUE@ENV{ID_MODEL}=="*Bitfury*BF1*", GOTO="bfgminer_add"
 @HAS_BITFORCE_TRUE@ENV{ID_MODEL}=="*BitFORCE*SHA256*", GOTO="bfgminer_add"
+@USE_DRILLBIT_TRUE@ENV{manufacturer}=="*Drillbit*", GOTO="bfgminer_add"
 @HAS_ICARUS_TRUE@ENV{ID_MODEL}=="*Cairnsmore1*", GOTO="bfgminer_add"
 @HAS_ICARUS_TRUE@ENV{ID_MODEL}=="*Block*Erupter*", GOTO="bfgminer_add"
 @USE_HASHBUSTER_TRUE@ENV{ID_MODEL}=="*HashBuster*", GOTO="bfgminer_add"

+ 4 - 0
Makefile.am

@@ -218,6 +218,10 @@ if HAS_BIGPIC
 bfgminer_SOURCES += driver-bigpic.c driver-bigpic.h
 endif
 
+if USE_DRILLBIT
+bfgminer_SOURCES += driver-drillbit.c
+endif
+
 if HAS_TWINFURY
 bfgminer_SOURCES += driver-twinfury.c driver-twinfury.h
 endif

+ 25 - 0
configure.ac

@@ -693,6 +693,31 @@ if test "x$bigpic" = "xyes"; then
 fi
 AM_CONDITIONAL([HAS_BIGPIC], [test x$bigpic = xyes])
 
+
+driverlist="$driverlist drillbit"
+AC_ARG_ENABLE([drillbit],
+	[AC_HELP_STRING([--disable-drillbit],[Compile support for DrillBit (default enabled)])],
+	[drillbit=$enableval],
+	[drillbit=auto]
+	)
+if test "x$drillbit" = "xno"; then
+	true
+elif test "x$bitfury" = "xyes"; then
+	drillbit=yes
+elif test "x$drillbit" = "xyes"; then
+	AC_MSG_ERROR([You explicitly disabled Bitfury and explicitly enabled DrillBit])
+else
+	drillbit=no
+fi
+if test "x$drillbit" = "xyes"; then
+	AC_DEFINE([USE_DRILLBIT], [1], [Defined to 1 if DrillBit support is wanted])
+	need_lowl_vcom=yes
+	has_asic=yes
+	have_udevrules=true
+fi
+AM_CONDITIONAL([USE_DRILLBIT], [test x$drillbit = xyes])
+
+
 driverlist="$driverlist twinfury"
 twinfury=auto
 AC_ARG_ENABLE([twinfury],

+ 711 - 0
driver-drillbit.c

@@ -0,0 +1,711 @@
+/*
+ * Copyright 2013 Luke Dashjr
+ * Copyright 2013 Angus Gratton
+ *
+ * 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 3 of the License, or (at your option)
+ * any later version.  See COPYING for more details.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "deviceapi.h"
+#include "logging.h"
+#include "lowlevel.h"
+#include "lowl-vcom.h"
+
+BFG_REGISTER_DRIVER(drillbit_drv)
+
+#define DRILLBIT_MIN_VERSION 2
+#define DRILLBIT_MAX_VERSION 3
+
+#define DRILLBIT_MAX_WORK_RESULTS 0x400
+#define DRILLBIT_MAX_RESULT_NONCES 0x10
+
+enum drillbit_capability {
+	DBC_TEMP      = 1,
+	DBC_EXT_CLOCK = 2,
+};
+
+enum drillbit_voltagecfg {
+	DBV_650mV = 0,
+	DBV_750mV = 2,
+	DBV_850mV = 1,
+	DBV_950mV = 3,
+};
+
+struct drillbit_board {
+	enum drillbit_voltagecfg core_voltage_cfg;
+	unsigned clock_level;
+	bool clock_div2;
+	bool use_ext_clock;
+	unsigned ext_clock_freq;
+	bool need_reinit;
+	bool trigger_identify;
+};
+
+static
+bool drillbit_lowl_match(const struct lowlevel_device_info * const info)
+{
+	if (!lowlevel_match_id(info, &lowl_vcom, 0, 0))
+		return false;
+	return (info->manufacturer && strstr(info->manufacturer, "Drillbit"));
+}
+
+static
+bool drillbit_detect_one(const char * const devpath)
+{
+	uint8_t buf[0x10];
+	const int fd = serial_open(devpath, 0, 1, true);
+	if (fd == -1)
+		applogr(false, LOG_DEBUG, "%s: %s: Failed to open", __func__, devpath);
+	if (1 != write(fd, "I", 1))
+	{
+		applog(LOG_DEBUG, "%s: %s: Error writing 'I'", __func__, devpath);
+err:
+		serial_close(fd);
+		return false;
+	}
+	if (sizeof(buf) != serial_read(fd, buf, sizeof(buf)))
+	{
+		applog(LOG_DEBUG, "%s: %s: Short read in response to 'I'",
+		       __func__, devpath);
+		goto err;
+	}
+	serial_close(fd);
+	
+	const unsigned protover = buf[0];
+	const unsigned long serialno = (uint32_t)buf[9] | ((uint32_t)buf[0xa] << 8) | ((uint32_t)buf[0xb] << 16) | ((uint32_t)buf[0xc] << 24);
+	char * const product = (void*)&buf[1];
+	buf[9] = '\0';  // Ensure it is null-terminated (clobbers serial, but we already parsed it)
+	unsigned chips = buf[0xd];
+	uint16_t caps = (uint16_t)buf[0xe] | ((uint16_t)buf[0xf] << 8);
+	if (!product[0])
+		applogr(false, LOG_DEBUG, "%s: %s: Null product name", __func__, devpath);
+	if (!serialno)
+		applogr(false, LOG_DEBUG, "%s: %s: Serial number is zero", __func__, devpath);
+	if (!chips)
+		applogr(false, LOG_DEBUG, "%s: %s: No chips found", __func__, devpath);
+	
+	int loglev = LOG_WARNING;
+	if (!strcmp(product, "DRILLBIT"))
+	{
+		// Hack: first production firmwares all described themselves as DRILLBIT, so fill in the gaps
+		if (chips == 1)
+			strcpy(product, "Thumb");
+		else
+			strcpy(product, "Eight");
+	}
+	else
+	if (chips == 8 && !strcmp(product, "Eight"))
+	{}  // Known device
+	else
+	if (chips == 1 && !strcmp(product, "Thumb"))
+	{}  // Known device
+	else
+		loglev = LOG_DEBUG;
+	
+	if (protover < DRILLBIT_MIN_VERSION || (loglev == LOG_DEBUG && protover > DRILLBIT_MAX_VERSION))
+		applogr(false, loglev, "%s: %s: Unknown device protocol version %u.",
+		        __func__, devpath, protover);
+	if (protover > DRILLBIT_MAX_VERSION)
+		applogr(false, loglev, "%s: %s: Device firmware uses newer Drillbit protocol %u. We only support up to %u. Find a newer BFGMiner!",
+		        __func__, devpath, protover, (unsigned)DRILLBIT_MAX_VERSION);
+	
+	if (protover == 2 && chips == 1)
+		// Production firmware Thumbs don't set any capability bits, so fill in the EXT_CLOCK one
+		caps |= DBC_EXT_CLOCK;
+	
+	char *serno = malloc(9);
+	snprintf(serno, 9, "%08lx", serialno);
+	
+	if (chips > 0x100)
+	{
+		applog(LOG_WARNING, "%s: %s: %u chips reported, but driver only supports up to 256",
+		       __func__, devpath, chips);
+		chips = 0x100;
+	}
+	
+	struct cgpu_info * const cgpu = malloc(sizeof(*cgpu));
+	*cgpu = (struct cgpu_info){
+		.drv = &drillbit_drv,
+		.device_path = strdup(devpath),
+		.dev_product = strdup(product),
+		.dev_serial = serno,
+		.deven = DEV_ENABLED,
+		.procs = chips,
+		.threads = 1,
+		//.device_data = ,
+	};
+	return add_cgpu(cgpu);
+}
+
+static
+bool drillbit_lowl_probe(const struct lowlevel_device_info * const info)
+{
+	return vcom_lowl_probe_wrapper(info, drillbit_detect_one);
+}
+
+static
+void drillbit_problem(struct cgpu_info * const dev)
+{
+	struct thr_info * const master_thr = dev->thr[0];
+	
+	if (dev->device_fd != -1)
+	{
+		serial_close(dev->device_fd);
+		dev->device_fd = -1;
+	}
+	timer_set_delay_from_now(&master_thr->tv_poll, 5000000);
+}
+
+#define problem(...)  do{  \
+	drillbit_problem(dev);  \
+	applogr(__VA_ARGS__);  \
+}while(0)
+
+static
+bool drillbit_check_response(const char * const repr, const int fd, struct cgpu_info * const dev, const char expect)
+{
+	uint8_t ack;
+	if (1 != serial_read(fd, &ack, 1))
+		problem(false, LOG_ERR, "%s: Short read in response to '%c'",
+		        repr, expect);
+	if (ack != expect)
+		problem(false, LOG_ERR, "%s: Wrong response to '%c': %u",
+		        dev->dev_repr, expect, (unsigned)ack);
+	return true;
+}
+
+static
+bool drillbit_reset(struct cgpu_info * const dev)
+{
+	const int fd = dev->device_fd;
+	if (unlikely(fd == -1))
+		return false;
+	
+	if (1 != write(fd, "R", 1))
+		problem(false, LOG_ERR, "%s: Error writing reset command", dev->dev_repr);
+	
+	return drillbit_check_response(dev->dev_repr, fd, dev, 'R');
+}
+
+static
+bool drillbit_send_config(struct cgpu_info * const dev)
+{
+	const int fd = dev->device_fd;
+	if (unlikely(fd == -1))
+		return false;
+	
+	const struct drillbit_board * const board = dev->device_data;
+	const uint8_t buf[7] = {'C', board->core_voltage_cfg, board->clock_level, (board->clock_div2 ? 1 : 0), (board->use_ext_clock ? 1 : 0), board->ext_clock_freq};
+	
+	if (sizeof(buf) != write(fd, buf, sizeof(buf)))
+		problem(false, LOG_ERR, "%s: Error sending config", dev->dev_repr);
+	
+	return drillbit_check_response(dev->dev_repr, fd, dev, 'C');
+}
+
+static bool drillbit_resend_jobs(struct cgpu_info *proc);
+
+static
+bool drillbit_reconfigure(struct cgpu_info * const dev, const bool reopen)
+{
+	struct thr_info * const master_thr = dev->thr[0];
+	int fd = dev->device_fd;
+	if (reopen || fd == -1)
+	{
+		if (fd != -1)
+			serial_close(fd);
+		
+		dev->device_fd = fd = serial_open(dev->device_path, 0, 10, true);
+		if (fd == -1)
+			return false;
+	}
+	
+	if (!(drillbit_reset(dev) && drillbit_send_config(dev)))
+	{
+		serial_close(fd);
+		dev->device_fd = -1;
+		return false;
+	}
+	
+	for (struct cgpu_info *proc = dev; proc; proc = proc->next_proc)
+		drillbit_resend_jobs(proc);
+	
+	timer_set_delay_from_now(&master_thr->tv_poll, 10000);
+	
+	return true;
+}
+
+static
+bool drillbit_ensure_configured(struct cgpu_info * const dev)
+{
+	if (dev->device_fd != -1)
+		return true;
+	return drillbit_reconfigure(dev, false);
+}
+
+static
+bool drillbit_init(struct thr_info * const master_thr)
+{
+	struct cgpu_info * const dev = master_thr->cgpu;
+	
+	dev->device_fd = -1;
+	struct drillbit_board * const board = malloc(sizeof(*board));
+	dev->device_data = board;
+	*board = (struct drillbit_board){
+		.core_voltage_cfg = DBV_850mV,
+		.clock_level = 40,
+		.clock_div2 = false,
+		.use_ext_clock = false,
+		.ext_clock_freq = 200,
+	};
+	
+	drillbit_reconfigure(dev, false);
+	
+	return true;
+}
+
+static
+bool drillbit_job_prepare(struct thr_info * const thr, struct work * const work, __maybe_unused const uint64_t max_nonce)
+{
+	struct cgpu_info * const proc = thr->cgpu;
+	const int chipid = proc->proc_id;
+	struct cgpu_info * const dev = proc->device;
+	const int fd = dev->device_fd;
+	uint8_t buf[0x31];
+	
+	if (!drillbit_ensure_configured(dev))
+		return false;
+	
+	buf[0] = 'W';
+	buf[1] = chipid;
+	buf[2] = 0;  // high bits of chipid
+	memcpy(&buf[3], work->midstate, 0x20);
+	memcpy(&buf[0x23], &work->data[0x40], 0xc);
+	
+	if (sizeof(buf) != write(fd, buf, sizeof(buf)))
+		problem(false, LOG_ERR, "%"PRIpreprv": Error sending work %d",
+		        proc->proc_repr, work->id);
+	
+	if (!drillbit_check_response(proc->proc_repr, fd, dev, 'W'))
+		problem(false, LOG_ERR, "%"PRIpreprv": Error queuing work %d",
+		        proc->proc_repr, work->id);
+	
+	applog(LOG_DEBUG, "%"PRIpreprv": Queued work %d",
+	       proc->proc_repr, work->id);
+	
+	work->blk.nonce = 0xffffffff;
+	return true;
+}
+
+static
+bool drillbit_resend_jobs(struct cgpu_info * const proc)
+{
+	struct thr_info * const thr = proc->thr[0];
+	bool rv;
+	
+	if (thr->work)
+		if (!drillbit_job_prepare(thr, thr->work, 0))
+		{
+			applog(LOG_WARNING, "%"PRIpreprv": Failed to resend %s work",
+			       proc->proc_repr, "current");
+			rv = false;
+		}
+	if (thr->next_work)
+	{
+		if (!drillbit_job_prepare(thr, thr->next_work, 0))
+		{
+			applog(LOG_WARNING, "%"PRIpreprv": Failed to resend %s work",
+			       proc->proc_repr, "next");
+			rv = false;
+		}
+		if (!rv)
+		{
+			// Fake transition so we kinda recover eventually
+			mt_job_transition(thr);
+			job_start_complete(thr);
+			timer_set_now(&thr->tv_morework);
+		}
+	}
+	return rv;
+}
+
+static
+void drillbit_first_job_start(struct thr_info __maybe_unused * const thr)
+{
+	struct cgpu_info * const proc = thr->cgpu;
+	if (unlikely(!thr->work))
+	{
+		applog(LOG_DEBUG, "%"PRIpreprv": No current work, assuming immediate start",
+		       proc->proc_repr);
+		mt_job_transition(thr);
+		job_start_complete(thr);
+		timer_set_now(&thr->tv_morework);
+	}
+}
+
+static
+int64_t drillbit_job_process_results(struct thr_info *thr, struct work *work, bool stopping)
+{
+	return 0xbd000000;
+}
+
+static
+struct cgpu_info *drillbit_find_proc(struct cgpu_info * const dev, int chipid)
+{
+	struct cgpu_info *proc = dev;
+	for (int i = 0; i < chipid; ++i)
+	{
+		proc = proc->next_proc;
+		if (unlikely(!proc))
+			return NULL;
+	}
+	return proc;
+}
+
+static
+bool bitfury_fudge_nonce2(struct work * const work, uint32_t * const nonce_p)
+{
+	if (!work)
+		return false;
+	const uint32_t m7    = *((uint32_t *)&work->data[64]);
+	const uint32_t ntime = *((uint32_t *)&work->data[68]);
+	const uint32_t nbits = *((uint32_t *)&work->data[72]);
+	return bitfury_fudge_nonce(work->midstate, m7, ntime, nbits, nonce_p);
+}
+
+static
+bool drillbit_get_work_results(struct cgpu_info * const dev)
+{
+	const int fd = dev->device_fd;
+	if (fd == -1)
+		return false;
+	
+	uint8_t buf[4 + (4 * DRILLBIT_MAX_RESULT_NONCES)];
+	uint32_t total;
+	int i, j;
+	
+	if (1 != write(fd, "E", 1))
+		problem(false, LOG_ERR, "%s: Error sending request for work results", dev->dev_repr);
+	
+	if (sizeof(total) != serial_read(fd, &total, sizeof(total)))
+		problem(false, LOG_ERR, "%s: Short read in response to 'E'", dev->dev_repr);
+	total = le32toh(total);
+	
+	if (total > DRILLBIT_MAX_WORK_RESULTS)
+		problem(false, LOG_ERR, "%s: Impossible number of total work: %lu",
+		        dev->dev_repr, (unsigned long)total);
+	
+	for (i = 0; i < total; ++i)
+	{
+		if (sizeof(buf) != serial_read(fd, buf, sizeof(buf)))
+			problem(false, LOG_ERR, "%s: Short read on %dth total work",
+			        dev->dev_repr, i);
+		const int chipid = buf[0];
+		struct cgpu_info * const proc = drillbit_find_proc(dev, chipid);
+		struct thr_info * const thr = proc->thr[0];
+		if (unlikely(!proc))
+		{
+			applog(LOG_ERR, "%s: Unknown chip id %d", dev->dev_repr, chipid);
+			continue;
+		}
+		const bool is_idle = buf[3];
+		int nonces = buf[2];
+		if (nonces > DRILLBIT_MAX_RESULT_NONCES)
+		{
+			applog(LOG_ERR, "%"PRIpreprv": More than %d nonces claimed, impossible",
+			       proc->proc_repr, (int)DRILLBIT_MAX_RESULT_NONCES);
+			nonces = DRILLBIT_MAX_RESULT_NONCES;
+		}
+		applog(LOG_DEBUG, "%"PRIpreprv": Handling completion of %d nonces. is_idle=%d work=%p next_work=%p",
+		       proc->proc_repr, nonces, is_idle, thr->work, thr->next_work);
+		const uint32_t *nonce_p = (void*)&buf[4];
+		for (j = 0; j < nonces; ++j, ++nonce_p)
+		{
+			uint32_t nonce = bitfury_decnonce(*nonce_p);
+			if (bitfury_fudge_nonce2(thr->work, &nonce))
+				submit_nonce(thr, thr->work, nonce);
+			else
+			if (bitfury_fudge_nonce2(thr->next_work, &nonce))
+			{
+				applog(LOG_DEBUG, "%"PRIpreprv": Result for next work, transitioning",
+				       proc->proc_repr);
+				submit_nonce(thr, thr->next_work, nonce);
+				mt_job_transition(thr);
+				job_start_complete(thr);
+			}
+			else
+			if (bitfury_fudge_nonce2(thr->prev_work, &nonce))
+			{
+				applog(LOG_DEBUG, "%"PRIpreprv": Result for PREVIOUS work",
+				       proc->proc_repr);
+				submit_nonce(thr, thr->prev_work, nonce);
+			}
+			else
+				inc_hw_errors(thr, thr->work, nonce);
+		}
+		if (is_idle && thr->next_work)
+		{
+			applog(LOG_DEBUG, "%"PRIpreprv": Chip went idle without any results for next work",
+			       proc->proc_repr);
+			mt_job_transition(thr);
+			job_start_complete(thr);
+		}
+		if (!thr->next_work)
+			timer_set_now(&thr->tv_morework);
+	}
+	
+	return true;
+}
+
+static
+void drillbit_poll(struct thr_info * const master_thr)
+{
+	struct cgpu_info * const dev = master_thr->cgpu;
+	struct drillbit_board * const board = dev->device_data;
+	
+	if (!drillbit_ensure_configured(dev))
+		return;
+	
+	drillbit_get_work_results(dev);
+	
+	if (board->need_reinit)
+	{
+		applog(LOG_NOTICE, "%s: Reinitialisation needed for configuration changes",
+		       dev->dev_repr);
+		drillbit_reconfigure(dev, false);
+		board->need_reinit = false;
+	}
+	if (board->trigger_identify)
+	{
+		const int fd = dev->device_fd;
+		applog(LOG_DEBUG, "%s: Sending identify command", dev->dev_repr);
+		if (1 != write(fd, "L", 1))
+			applog(LOG_ERR, "%s: Error writing identify command", dev->dev_repr);
+		drillbit_check_response(dev->dev_repr, fd, dev, 'L');
+		board->trigger_identify = false;
+	}
+	
+	timer_set_delay_from_now(&master_thr->tv_poll, 10000);
+}
+
+static bool drillbit_identify(struct cgpu_info * const proc)
+{
+	struct cgpu_info * const dev = proc->device;
+	struct drillbit_board * const board = dev->device_data;
+	board->trigger_identify = true;
+	return true;
+}
+
+static
+bool drillbit_get_stats(struct cgpu_info * const dev)
+{
+	if (dev != dev->device)
+		return true;
+	
+	const int fd = dev->device_fd;
+	if (fd == -1)
+		return false;
+	
+	if (1 != write(fd, "T", 1))
+		problem(false, LOG_ERR, "%s: Error requesting temperature", dev->dev_repr);
+	
+	uint8_t buf[2];
+	
+	if (sizeof(buf) != serial_read(fd, buf, sizeof(buf)))
+		problem(false, LOG_ERR, "%s: Short read in response to 'T'", dev->dev_repr);
+	
+	float temp = ((uint16_t)buf[0]) | ((uint16_t)buf[1] << 8);
+	temp /= 10.;
+	for (struct cgpu_info *proc = dev; proc; proc = proc->next_proc)
+		proc->temp = temp;
+	
+	return true;
+}
+
+static
+float drillbit_voltagecfg_volts(const enum drillbit_voltagecfg vcfg)
+{
+	switch (vcfg)
+	{
+		case DBV_650mV: return 0.65;
+		case DBV_750mV: return 0.75;
+		case DBV_850mV: return 0.85;
+		case DBV_950mV: return 0.95;
+	}
+	return 0;
+}
+
+static
+void drillbit_clockcfg_str(char * const buf, size_t bufsz, struct drillbit_board * const board)
+{
+	if (board->use_ext_clock)
+		snprintf(buf, bufsz, "%u", board->ext_clock_freq);
+	else
+		snprintf(buf, bufsz, "L%u", board->clock_level);
+	if (board->clock_div2)
+		tailsprintf(buf, bufsz, ":2");
+}
+
+static
+struct api_data *drillbit_api_stats(struct cgpu_info * const proc)
+{
+	struct cgpu_info * const dev = proc->device;
+	struct drillbit_board * const board = dev->device_data;
+	struct api_data *root = NULL;
+	char buf[0x100];
+	
+	drillbit_clockcfg_str(buf, sizeof(buf), board);
+	root = api_add_string(root, "ClockCfg", buf, true);
+	
+	float volts = drillbit_voltagecfg_volts(board->core_voltage_cfg);
+	root = api_add_volts(root, "Voltage", &volts, true);
+	
+	return root;
+}
+
+static
+char *drillbit_set_device(struct cgpu_info * const proc, char * const option, char *setting, char * const replybuf)
+{
+	struct cgpu_info * const dev = proc->device;
+	struct drillbit_board * const board = dev->device_data;
+	
+	if (!strcasecmp(option, "help"))
+	{
+		sprintf(replybuf,
+			"voltage: 0.65, 0.75, 0.85, or 0.95 (volts)\n"
+			"clock: 80-230 (MHz) using external clock, or L0-L63 for internal clock levels; append :2 to activate div2"
+		);
+		return replybuf;
+	}
+	
+	if (!strcasecmp(option, "voltage"))
+	{
+		// NOTE: Do not use replybuf in here without implementing it in drillbit_tui_handle_choice
+		if (!setting || !*setting)
+			return "Missing voltage setting";
+		const int val = atof(setting) * 1000;
+		enum drillbit_voltagecfg vcfg;
+		switch (val)
+		{
+			case 650: case 649:
+				vcfg = DBV_650mV;
+				break;
+			case 750: case 749:
+				vcfg = DBV_750mV;
+				break;
+			case 850: case 849:
+				vcfg = DBV_850mV;
+				break;
+			case 950: case 949:
+				vcfg = DBV_950mV;
+				break;
+			default:
+				return "Invalid voltage value";
+		}
+		
+		board->core_voltage_cfg = vcfg;
+		board->need_reinit = true;
+		
+		return NULL;
+	}
+	
+	if (!strcasecmp(option, "clock"))
+	{
+		// NOTE: Do not use replybuf in here without implementing it in drillbit_tui_handle_choice
+		const bool use_ext_clock = !(setting[0] == 'L');
+		char *end = &setting[use_ext_clock ? 0 : 1];
+		const unsigned num = strtol(end, &end, 0);
+		const bool div2 = (end[0] == ':' && end[1] == '2');
+		// NOTE: board assignments are ordered such that it is safe to race
+		if (use_ext_clock)
+		{
+			if (num < 80 || num > 230)
+				return "External clock frequency out of range (80-230)";
+			board->clock_div2 = div2;
+			board->ext_clock_freq = num;
+			board->use_ext_clock = true;
+		}
+		else
+		{
+			if (num < 0 || num > 63)
+				return "Internal clock level out of range (0-63)";
+			board->clock_div2 = div2;
+			board->clock_level = num;
+			board->use_ext_clock = false;
+		}
+		board->need_reinit = true;
+		return NULL;
+	}
+	
+	sprintf(replybuf, "Unknown option: %s", option);
+	return replybuf;
+}
+
+#ifdef HAVE_CURSES
+static
+void drillbit_tui_wlogprint_choices(struct cgpu_info * const proc)
+{
+	wlogprint("[C]lock [V]oltage ");
+}
+
+static
+const char *drillbit_tui_handle_choice(struct cgpu_info * const proc, const int input)
+{
+	char *val;
+	switch (input)
+	{
+		case 'c': case 'C':
+			val = curses_input("Set clock (80-230 MHz using external clock, or L0-L63 for internal clock levels; append :2 to activate div2");
+			return drillbit_set_device(proc, "clock", val, NULL) ?: "Requesting clock change";
+		case 'v': case 'V':
+			val = curses_input("Set voltage (0.65, 0.75, 0.85, or 0.95)");
+			return drillbit_set_device(proc, "voltage", val, NULL) ?: "Requesting voltage change";
+	}
+	return NULL;
+}
+
+static
+void drillbit_wlogprint_status(struct cgpu_info * const proc)
+{
+	struct cgpu_info * const dev = proc->device;
+	struct drillbit_board * const board = dev->device_data;
+	char buf[0x100];
+	
+	drillbit_clockcfg_str(buf, sizeof(buf), board);
+	wlogprint("Clock: %s\n", buf);
+	wlogprint("Voltage: %.2f\n", drillbit_voltagecfg_volts(board->core_voltage_cfg));
+}
+#endif
+
+struct device_drv drillbit_drv = {
+	.dname = "drillbit",
+	.name = "DRB",
+	
+	.lowl_match = drillbit_lowl_match,
+	.lowl_probe = drillbit_lowl_probe,
+	
+	.thread_init = drillbit_init,
+	
+	.minerloop = minerloop_async,
+	.job_prepare = drillbit_job_prepare,
+	.job_start = drillbit_first_job_start,
+	.job_process_results = drillbit_job_process_results,
+	.poll = drillbit_poll,
+	.get_stats = drillbit_get_stats,
+	.identify_device = drillbit_identify,
+	
+	.get_api_stats = drillbit_api_stats,
+	.set_device = drillbit_set_device,
+#ifdef HAVE_CURSES
+	.proc_wlogprint_status = drillbit_wlogprint_status,
+	.proc_tui_wlogprint_choices = drillbit_tui_wlogprint_choices,
+	.proc_tui_handle_choice = drillbit_tui_handle_choice,
+#endif
+};