From 3578ec6a3d7187438c4093f4bb7ac2bb6d4184cb Mon Sep 17 00:00:00 2001 From: Marc Schink Date: Thu, 17 Mar 2016 16:23:03 +0100 Subject: 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 Reviewed-on: https://review.coreboot.org/c/28087 Tested-by: build bot (Jenkins) Reviewed-by: Nico Huber --- Makefile | 57 ++++++- README | 1 + flashrom.8.tmpl | 56 +++++++ flashrom.c | 12 ++ jlink_spi.c | 456 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ programmer.h | 11 ++ 6 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 jlink_spi.c diff --git a/Makefile b/Makefile index 1ff578c0..1e7db3ed 100644 --- a/Makefile +++ b/Makefile @@ -656,6 +656,9 @@ CONFIG_CH341A_SPI ?= yes # 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 @@ PROGRAMMER_OBJS += digilent_spi.o 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 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 @@ ifeq ($(ARCH), x86) 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 @@ int main(int argc, char **argv) endef export LIBUSB1_TEST +define LIBJAYLINK_TEST +#include +#include +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 @@ ifeq ($(CHECK_LIBUSB1), yes) 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 b5ed9460..4e7bd4fb 100644 --- a/README +++ b/README @@ -50,6 +50,7 @@ To build flashrom you need to install the following software: * 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 c557af74..c1a228be 100644 --- a/flashrom.8.tmpl +++ b/flashrom.8.tmpl @@ -331,6 +331,8 @@ bitbanging adapter) .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 @@ can be (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 59a7531f..7129a51a 100644 --- a/flashrom.c +++ b/flashrom.c @@ -437,6 +437,18 @@ const struct programmer_entry programmer_table[] = { }, #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 00000000..08a9ba98 --- /dev/null +++ b/jlink_spi.c @@ -0,0 +1,456 @@ +/* + * This file is part of the flashrom project. + * + * Copyright (C) 2016 Marc Schink + * + * 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 +#include +#include +#include +#include +#include + +#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 311992a0..8bf7d294 100644 --- a/programmer.h +++ b/programmer.h @@ -117,6 +117,9 @@ enum programmer { #endif #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 @@ int digilent_spi_init(void); 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 @@ enum spi_controller { #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 -- cgit v1.2.3