From 412ae53aadb53cd63e754d638bafe6e426aeafee Mon Sep 17 00:00:00 2001 From: "Albert ARIBAUD \\(3ADEV\\)" Date: Tue, 31 Mar 2015 11:40:51 +0200 Subject: lpc32xx: add support for board work_92105 Work_92105 from Work Microwave is an LPC3250- based board with the following features: - 64MB or 128MB SDR DRAM - 1 GB SLC NAND, managed through MLC controller. - Ethernet - Ethernet + PHY SMSC8710 - I2C: - EEPROM (24M01-compatible) - RTC (DS1374-compatible) - Temperature sensor (DS620) - DACs (2 x MAX518) - SPI (through SSP interface) - Port expander MAX6957 - LCD display (HD44780-compatible), controlled through the port expander and DACs This board has SPL support, and uses the LPC32XX boot image format. Signed-off-by: Albert ARIBAUD (3ADEV) --- board/work-microwave/work_92105/Kconfig | 15 + board/work-microwave/work_92105/MAINTAINERS | 6 + board/work-microwave/work_92105/Makefile | 10 + board/work-microwave/work_92105/README | 91 ++++++ board/work-microwave/work_92105/work_92105.c | 77 +++++ .../work-microwave/work_92105/work_92105_display.c | 349 +++++++++++++++++++++ .../work-microwave/work_92105/work_92105_display.h | 14 + board/work-microwave/work_92105/work_92105_spl.c | 85 +++++ 8 files changed, 647 insertions(+) create mode 100644 board/work-microwave/work_92105/Kconfig create mode 100644 board/work-microwave/work_92105/MAINTAINERS create mode 100644 board/work-microwave/work_92105/Makefile create mode 100644 board/work-microwave/work_92105/README create mode 100644 board/work-microwave/work_92105/work_92105.c create mode 100644 board/work-microwave/work_92105/work_92105_display.c create mode 100644 board/work-microwave/work_92105/work_92105_display.h create mode 100644 board/work-microwave/work_92105/work_92105_spl.c (limited to 'board') diff --git a/board/work-microwave/work_92105/Kconfig b/board/work-microwave/work_92105/Kconfig new file mode 100644 index 0000000000..74f004f53c --- /dev/null +++ b/board/work-microwave/work_92105/Kconfig @@ -0,0 +1,15 @@ +if TARGET_WORK_92105 + +config SYS_BOARD + default "work_92105" + +config SYS_VENDOR + default "work-microwave" + +config SYS_SOC + default "lpc32xx" + +config SYS_CONFIG_NAME + default "work_92105" + +endif diff --git a/board/work-microwave/work_92105/MAINTAINERS b/board/work-microwave/work_92105/MAINTAINERS new file mode 100644 index 0000000000..29a92c5ffe --- /dev/null +++ b/board/work-microwave/work_92105/MAINTAINERS @@ -0,0 +1,6 @@ +WORK_92105 BOARD +M: Albert ARIBAUD +S: Maintained +F: board/work-microwave/work_92105/ +F: include/configs/work_92105.h +F: configs/work_92105_defconfig diff --git a/board/work-microwave/work_92105/Makefile b/board/work-microwave/work_92105/Makefile new file mode 100644 index 0000000000..ba31c8e4f4 --- /dev/null +++ b/board/work-microwave/work_92105/Makefile @@ -0,0 +1,10 @@ +# +# (C) Copyright 2014 DENX Software Engineering GmbH +# Written-by: Albert ARIBAUD +# +# SPDX-License-Identifier: GPL-2.0+ +# + +obj-y := work_92105.o work_92105_display.o + +obj-$(CONFIG_SPL_BUILD) += work_92105_spl.o diff --git a/board/work-microwave/work_92105/README b/board/work-microwave/work_92105/README new file mode 100644 index 0000000000..3c256e0b2c --- /dev/null +++ b/board/work-microwave/work_92105/README @@ -0,0 +1,91 @@ +Work_92105 from Work Microwave is an LPC3250- based board with the +following features: + + - 64MB SDR DRAM + - 1 GB SLC NAND, managed through MLC controller. + - Ethernet + - Ethernet + PHY SMSC8710 + - I2C: + - EEPROM (24M01-compatible) + - RTC (DS1374-compatible) + - Temperature sensor (DS620) + - DACs (2 x MAX518) + - SPI (through SSP interface) + - Port expander MAX6957 + - LCD display (HD44780-compatible), controlled + through the port expander and DACs + +Standard SPL and U-Boot binaries +-------------------------------- + +The default 'make' (or the 'make all') command will produce the +following files: + +1. spl/u-boot-spl.bin SPL, intended to run from SRAM at address 0. + This file can be loaded in SRAM through a JTAG + debugger or through the LPC32XX Service Boot + mechanism. + +2. u-boot.bin The raw U-Boot image, which can be loaded in + DDR through a JTAG debugger (for instance by + breaking SPL after DDR init), or by a running + U-Boot through e.g. 'loady' or 'tftp' and then + executed with 'go'. + +3. u-boot.img A U-Boot image with a mkimage header prepended. + SPL assumes (even when loaded through JTAG or + Service Boot) that such an image will be found + at offset 0x00040000 in NAND. + +NAND cold-boot binaries +----------------------- + +The board can boot entirely from power-on with only SPL and U-Boot in +NAND. The LPC32XX-specific 'make lpc32xx-full.bin' command will produce +(in addition to spl/u-boot-spl.bin and u-boot.img if they were not made +already) the following files: + +4. lpc32xx-spl.img spl/u-boot-spl.bin, with a LPC32XX boot header + prepended. This header is required for the ROM + code to load SPL into SRAM and branch into it. + The content of this file is expected to reside + in NAND at addresses 0x00000000 and 0x00020000 + (two copies). + +5. lpc32xx-boot-0.bin lpc32xx-spl.img, padded with 0xFF bytes to a + size of 0x20000 bytes. This file covers exactly + the reserved area for the first bootloader copy + in NAND. + +6. lpc32xx-boot-1.bin Same as lpc32xx-boot-0.bin. This is intended to + be used as the second bootloader copy. + +7. lpc32xx-full.bin lpc32xx-boot-0.bin, lpc32xx-boot-1.bin and + u-boot.img concatenated. This file represents + the content of whole bootloader as present in + NAND at offset 00x00000000. + +Flashing instructions +--------------------- + +The following assumes a working U-Boot on the target, with the ability +to load files into DDR. + +To update the whole bootloader: + + nand erase 0x00000000 0x80000 + (load lpc32xx-full.bin at location $loadaddr) + nand write $loadaddr 0x00000000 $filesize + +To update SPL only (note the double nand write) : + + nand erase 0x00000000 0x40000 + (load lpc32xx-spl.img or lpc32xx-boot-N.bin at location $loadaddr) + nand write $loadaddr 0x00000000 $filesize + nand write $loadaddr 0x00020000 $filesize + +To update U-Boot only: + + nand erase 0x00040000 0x40000 + (load u-boot.img at location $loadaddr) + nand write $loadaddr 0x00040000 $filesize diff --git a/board/work-microwave/work_92105/work_92105.c b/board/work-microwave/work_92105/work_92105.c new file mode 100644 index 0000000000..f782284d60 --- /dev/null +++ b/board/work-microwave/work_92105/work_92105.c @@ -0,0 +1,77 @@ +/* + * WORK Microwave work_92105 board support + * + * (C) Copyright 2014 DENX Software Engineering GmbH + * Written-by: Albert ARIBAUD + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "work_92105_display.h" + +DECLARE_GLOBAL_DATA_PTR; + +static struct clk_pm_regs *clk = (struct clk_pm_regs *)CLK_PM_BASE; +static struct wdt_regs *wdt = (struct wdt_regs *)WDT_BASE; + +void reset_periph(void) +{ + setbits_le32(&clk->timclk_ctrl, CLK_TIMCLK_WATCHDOG); + writel(WDTIM_MCTRL_RESFRC1, &wdt->mctrl); + udelay(150); + writel(0, &wdt->mctrl); + clrbits_le32(&clk->timclk_ctrl, CLK_TIMCLK_WATCHDOG); +} + +int board_early_init_f(void) +{ + /* initialize serial port for console */ + lpc32xx_uart_init(CONFIG_SYS_LPC32XX_UART); + /* enable I2C, SSP, MAC, NAND */ + lpc32xx_i2c_init(1); /* only I2C1 has devices, I2C2 has none */ + lpc32xx_ssp_init(); + lpc32xx_mac_init(); + lpc32xx_mlc_nand_init(); + /* Display must wait until after relocation and devices init */ + return 0; +} + +#define GPO_19 115 + +int board_early_init_r(void) +{ + /* Set NAND !WP to 1 through GPO_19 */ + gpio_request(GPO_19, "NAND_nWP"); + gpio_direction_output(GPO_19, 1); + + /* initialize display */ + work_92105_display_init(); + + return 0; +} + +int board_init(void) +{ + reset_periph(); + /* adress of boot parameters */ + gd->bd->bi_boot_params = CONFIG_SYS_SDRAM_BASE + 0x100; + + return 0; +} + +int dram_init(void) +{ + gd->ram_size = get_ram_size((void *)CONFIG_SYS_SDRAM_BASE, + CONFIG_SYS_SDRAM_SIZE); + + return 0; +} diff --git a/board/work-microwave/work_92105/work_92105_display.c b/board/work-microwave/work_92105/work_92105_display.c new file mode 100644 index 0000000000..c8b10131f9 --- /dev/null +++ b/board/work-microwave/work_92105/work_92105_display.c @@ -0,0 +1,349 @@ +/* + * work_92105 display support + * + * (C) Copyright 2014 DENX Software Engineering GmbH + * Written-by: Albert ARIBAUD + * + * The work_92105 display is a HD44780-compatible module + * controlled through a MAX6957AAX SPI port expander, two + * MAX518 I2C DACs and native LPC32xx GPO 15. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * GPO 15 in port 3 is gpio 3*32+15 = 111 + */ + +#define GPO_15 111 + +/** + * MAX6957AAX registers that we will be using + */ + +#define MAX6957_CONF 0x04 + +#define MAX6957_CONF_08_11 0x0A +#define MAX6957_CONF_12_15 0x0B +#define MAX6957_CONF_16_19 0x0C + +/** + * Individual gpio ports (one per gpio) to HD44780 + */ + +#define MAX6957AAX_HD44780_RS 0x29 +#define MAX6957AAX_HD44780_R_W 0x2A +#define MAX6957AAX_HD44780_EN 0x2B +#define MAX6957AAX_HD44780_DATA 0x4C + +/** + * Display controller instructions + */ + +/* Function set: eight bits, two lines, 8-dot font */ +#define HD44780_FUNCTION_SET 0x38 + +/* Display ON / OFF: turn display on */ +#define HD44780_DISPLAY_ON_OFF_CONTROL 0x0C + +/* Entry mode: increment */ +#define HD44780_ENTRY_MODE_SET 0x06 + +/* Clear */ +#define HD44780_CLEAR_DISPLAY 0x01 + +/* Set DDRAM addr (to be ORed with exact address) */ +#define HD44780_SET_DDRAM_ADDR 0x80 + +/* Set CGRAM addr (to be ORed with exact address) */ +#define HD44780_SET_CGRAM_ADDR 0x40 + +/** + * Default value for contrats + */ + +#define CONTRAST_DEFAULT 25 + +/** + * Define slave as a module-wide local to save passing it around, + * plus we will need it after init for the "hd44780" command. + */ + +static struct spi_slave *slave; + +/* + * Write a value into a MAX6957AAX register. + */ + +static void max6957aax_write(uint8_t reg, uint8_t value) +{ + uint8_t dout[2]; + + dout[0] = reg; + dout[1] = value; + gpio_set_value(GPO_15, 0); + /* do SPI read/write (passing din==dout is OK) */ + spi_xfer(slave, 16, dout, dout, SPI_XFER_BEGIN | SPI_XFER_END); + gpio_set_value(GPO_15, 1); +} + +/* + * Read a value from a MAX6957AAX register. + * + * According to the MAX6957AAX datasheet, we should release the chip + * select halfway through the read sequence, when the actual register + * value is read; but the WORK_92105 hardware prevents the MAX6957AAX + * SPI OUT from reaching the LPC32XX SIP MISO if chip is not selected. + * so let's release the CS an hold it again while reading the result. + */ + +static uint8_t max6957aax_read(uint8_t reg) +{ + uint8_t dout[2], din[2]; + + /* send read command */ + dout[0] = reg | 0x80; /* set bit 7 to indicate read */ + dout[1] = 0; + gpio_set_value(GPO_15, 0); + /* do SPI read/write (passing din==dout is OK) */ + spi_xfer(slave, 16, dout, dout, SPI_XFER_BEGIN | SPI_XFER_END); + /* latch read command */ + gpio_set_value(GPO_15, 1); + /* read register -- din = noop on xmit, din[1] = reg on recv */ + din[0] = 0; + din[1] = 0; + gpio_set_value(GPO_15, 0); + /* do SPI read/write (passing din==dout is OK) */ + spi_xfer(slave, 16, din, din, SPI_XFER_BEGIN | SPI_XFER_END); + /* end of read. */ + gpio_set_value(GPO_15, 1); + return din[1]; +} + +static void hd44780_instruction(unsigned long instruction) +{ + max6957aax_write(MAX6957AAX_HD44780_RS, 0); + max6957aax_write(MAX6957AAX_HD44780_R_W, 0); + max6957aax_write(MAX6957AAX_HD44780_EN, 1); + max6957aax_write(MAX6957AAX_HD44780_DATA, instruction); + max6957aax_write(MAX6957AAX_HD44780_EN, 0); + /* HD44780 takes 37 us for most instructions, 1520 for clear */ + if (instruction == HD44780_CLEAR_DISPLAY) + udelay(2000); + else + udelay(100); +} + +static void hd44780_write_char(char c) +{ + max6957aax_write(MAX6957AAX_HD44780_RS, 1); + max6957aax_write(MAX6957AAX_HD44780_R_W, 0); + max6957aax_write(MAX6957AAX_HD44780_EN, 1); + max6957aax_write(MAX6957AAX_HD44780_DATA, c); + max6957aax_write(MAX6957AAX_HD44780_EN, 0); + /* HD44780 takes 37 us to write to DDRAM or CGRAM */ + udelay(100); +} + +static void hd44780_write_str(char *s) +{ + max6957aax_write(MAX6957AAX_HD44780_RS, 1); + max6957aax_write(MAX6957AAX_HD44780_R_W, 0); + while (*s) { + max6957aax_write(MAX6957AAX_HD44780_EN, 1); + max6957aax_write(MAX6957AAX_HD44780_DATA, *s); + max6957aax_write(MAX6957AAX_HD44780_EN, 0); + s++; + /* HD44780 takes 37 us to write to DDRAM or CGRAM */ + udelay(100); + } +} + +/* + * Existing user code might expect these custom characters to be + * recognized and displayed on the LCD + */ + +static u8 char_gen_chars[] = { + /* #8, empty rectangle */ + 0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F, + /* #9, filled right arrow */ + 0x10, 0x18, 0x1C, 0x1E, 0x1C, 0x18, 0x10, 0x00, + /* #10, filled left arrow */ + 0x01, 0x03, 0x07, 0x0F, 0x07, 0x03, 0x01, 0x00, + /* #11, up and down arrow */ + 0x04, 0x0E, 0x1F, 0x00, 0x00, 0x1F, 0x0E, 0x04, + /* #12, plus/minus */ + 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00, 0x1F, 0x00, + /* #13, fat exclamation mark */ + 0x06, 0x06, 0x06, 0x06, 0x00, 0x06, 0x06, 0x00, + /* #14, empty square */ + 0x00, 0x1F, 0x11, 0x11, 0x11, 0x1F, 0x00, 0x00, + /* #15, struck out square */ + 0x00, 0x1F, 0x19, 0x15, 0x13, 0x1F, 0x00, 0x00, +}; + +static void hd44780_init_char_gen(void) +{ + int i; + + hd44780_instruction(HD44780_SET_CGRAM_ADDR); + + for (i = 0; i < sizeof(char_gen_chars); i++) + hd44780_write_char(char_gen_chars[i]); + + hd44780_instruction(HD44780_SET_DDRAM_ADDR); +} + +void work_92105_display_init(void) +{ + int claim_err; + char *display_contrast_str; + uint8_t display_contrast = CONTRAST_DEFAULT; + uint8_t enable_backlight = 0x96; + + slave = spi_setup_slave(0, 0, 500000, 0); + + if (!slave) { + printf("Failed to set up SPI slave\n"); + return; + } + + claim_err = spi_claim_bus(slave); + + if (claim_err) + debug("Failed to claim SPI bus: %d\n", claim_err); + + /* enable backlight */ + i2c_write(0x2c, 0x01, 1, &enable_backlight, 1); + + /* set display contrast */ + display_contrast_str = getenv("fwopt_dispcontrast"); + if (display_contrast_str) + display_contrast = simple_strtoul(display_contrast_str, + NULL, 10); + i2c_write(0x2c, 0x00, 1, &display_contrast, 1); + + /* request GPO_15 as an output initially set to 1 */ + gpio_request(GPO_15, "MAX6957_nCS"); + gpio_direction_output(GPO_15, 1); + + /* enable MAX6957 portexpander */ + max6957aax_write(MAX6957_CONF, 0x01); + /* configure pin 8 as input, pins 9..19 as outputs */ + max6957aax_write(MAX6957_CONF_08_11, 0x56); + max6957aax_write(MAX6957_CONF_12_15, 0x55); + max6957aax_write(MAX6957_CONF_16_19, 0x55); + + /* initialize HD44780 */ + max6957aax_write(MAX6957AAX_HD44780_EN, 0); + hd44780_instruction(HD44780_FUNCTION_SET); + hd44780_instruction(HD44780_DISPLAY_ON_OFF_CONTROL); + hd44780_instruction(HD44780_ENTRY_MODE_SET); + + /* write custom character glyphs */ + hd44780_init_char_gen(); + + /* Show U-Boot version, date and time as a sign-of-life */ + hd44780_instruction(HD44780_CLEAR_DISPLAY); + hd44780_instruction(HD44780_SET_DDRAM_ADDR | 0); + hd44780_write_str(U_BOOT_VERSION); + hd44780_instruction(HD44780_SET_DDRAM_ADDR | 64); + hd44780_write_str(U_BOOT_DATE); + hd44780_instruction(HD44780_SET_DDRAM_ADDR | 64 | 20); + hd44780_write_str(U_BOOT_TIME); +} + +#ifdef CONFIG_CMD_MAX6957 + +static int do_max6957aax(cmd_tbl_t *cmdtp, int flag, int argc, + char *const argv[]) +{ + int reg, val; + + if (argc != 3) + return CMD_RET_USAGE; + switch (argv[1][0]) { + case 'r': + case 'R': + reg = simple_strtoul(argv[2], NULL, 0); + val = max6957aax_read(reg); + printf("MAX6957 reg 0x%02x read 0x%02x\n", reg, val); + return 0; + default: + reg = simple_strtoul(argv[1], NULL, 0); + val = simple_strtoul(argv[2], NULL, 0); + max6957aax_write(reg, val); + printf("MAX6957 reg 0x%02x wrote 0x%02x\n", reg, val); + return 0; + } + return 1; +} + +#ifdef CONFIG_SYS_LONGHELP +static char max6957aax_help_text[] = + "max6957aax - write or read display register:\n" + "\tmax6957aax R|r reg - read display register;\n" + "\tmax6957aax reg val - write display register."; +#endif + +U_BOOT_CMD( + max6957aax, 6, 1, do_max6957aax, + "SPI MAX6957 display write/read", + max6957aax_help_text +); +#endif /* CONFIG_CMD_MAX6957 */ + +#ifdef CONFIG_CMD_HD44760 + +/* + * We need the HUSH parser because we need string arguments, and + * only HUSH can understand them. + */ + +#if !defined(CONFIG_SYS_HUSH_PARSER) +#error CONFIG_CMD_HD44760 requires CONFIG_SYS_HUSH_PARSER +#endif + +static int do_hd44780(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[]) +{ + char *cmd; + + if (argc != 3) + return CMD_RET_USAGE; + + cmd = argv[1]; + + if (strcasecmp(cmd, "cmd") == 0) + hd44780_instruction(simple_strtol(argv[2], NULL, 0)); + else if (strcasecmp(cmd, "data") == 0) + hd44780_write_char(simple_strtol(argv[2], NULL, 0)); + else if (strcasecmp(cmd, "str") == 0) + hd44780_write_str(argv[2]); + return 0; +} + +#ifdef CONFIG_SYS_LONGHELP +static char hd44780_help_text[] = + "hd44780 - control LCD driver:\n" + "\thd44780 cmd - send command to driver;\n" + "\thd44780 data - send data to driver;\n" + "\thd44780 str \"\" - send \"\" to driver."; +#endif + +U_BOOT_CMD( + hd44780, 6, 1, do_hd44780, + "HD44780 LCD driver control", + hd44780_help_text +); +#endif /* CONFIG_CMD_HD44780 */ diff --git a/board/work-microwave/work_92105/work_92105_display.h b/board/work-microwave/work_92105/work_92105_display.h new file mode 100644 index 0000000000..dd6e768eaf --- /dev/null +++ b/board/work-microwave/work_92105/work_92105_display.h @@ -0,0 +1,14 @@ +/* + * work_92105 display support interface + * + * (C) Copyright 2014 DENX Software Engineering GmbH + * Written-by: Albert ARIBAUD + * + * The work_92105 display is a HD44780-compatible module + * controlled through a MAX6957AAX SPI port expander, two + * MAX518 I2C DACs and native LPC32xx GPO 15. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +void work_92105_display_init(void); diff --git a/board/work-microwave/work_92105/work_92105_spl.c b/board/work-microwave/work_92105/work_92105_spl.c new file mode 100644 index 0000000000..282a6dd3a7 --- /dev/null +++ b/board/work-microwave/work_92105/work_92105_spl.c @@ -0,0 +1,85 @@ +/* + * WORK Microwave work_92105 board support + * + * (C) Copyright 2014 DENX Software Engineering GmbH + * Written-by: Albert ARIBAUD + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include +#include +#include +#include +#include +#include +#include +#include "work_92105_display.h" + +struct emc_dram_settings dram_64mb = { + .cmddelay = 0x0001C000, + .config0 = 0x00005682, + .rascas0 = 0x00000302, + .rdconfig = 0x00000011, + .trp = 52631578, + .tras = 20833333, + .tsrex = 12500000, + .twr = 66666666, + .trc = 13888888, + .trfc = 10256410, + .txsr = 12500000, + .trrd = 1, + .tmrd = 1, + .tcdlr = 0, + .refresh = 128000, + .mode = 0x00018000, + .emode = 0x02000000 +}; + +const struct emc_dram_settings dram_128mb = { + .cmddelay = 0x0001C000, + .config0 = 0x00005882, + .rascas0 = 0x00000302, + .rdconfig = 0x00000011, + .trp = 52631578, + .tras = 22222222, + .tsrex = 8333333, + .twr = 66666666, + .trc = 14814814, + .trfc = 10256410, + .txsr = 8333333, + .trrd = 1, + .tmrd = 1, + .tcdlr = 0, + .refresh = 128000, + .mode = 0x00030000, + .emode = 0x02000000 +}; + +void spl_board_init(void) +{ + /* initialize serial port for console */ + lpc32xx_uart_init(CONFIG_SYS_LPC32XX_UART); + /* initialize console */ + preloader_console_init(); + /* init DDR and NAND to chainload U-Boot */ + ddr_init(&dram_128mb); + /* + * If this is actually a 64MB module, then the highest column + * bit in any address will be ignored, and thus address 0x80000000 + * should be mirrored at address 0x80000800. Test this. + */ + writel(0x31415926, 0x80000000); /* write Pi at 0x80000000 */ + writel(0x16180339, 0x80000800); /* write Phi at 0x80000800 */ + if (readl(0x80000000) == 0x16180339) /* check 0x80000000 */ { + /* actually 64MB mirrored: reconfigure controller */ + ddr_init(&dram_64mb); + } + /* initialize NAND controller to load U-Boot from NAND */ + lpc32xx_mlc_nand_init(); +} + +u32 spl_boot_device(void) +{ + return BOOT_DEVICE_NAND; +} -- cgit v1.2.1