flash/nor: add support for NXP QN908x 84/5584/11
authoriosabi <iosabi@protonmail.com>
Thu, 9 Apr 2020 22:00:58 +0000 (22:00 +0000)
committerAntonio Borneo <borneo.antonio@gmail.com>
Sat, 10 Jun 2023 16:58:35 +0000 (16:58 +0000)
This patch adds support for the NXP QN908x family of Bluetooth
microcontrollers, such as the QN9080. This chip features a Cortex-M4F
with 512 KiB of flash on all the available versions, although the
documentation suggests that there might be 256 kB versions as well.

The initial support allows to read, erase and write the whole user flash
area. Three new sub-commands under the new "qn908x" command are added
in this patch as well: disable_wdog to disabled the watchdog,
mass_erase to perform a mass erase and allow_brick to allow programming
images that disable the SWD interface.

Disabling the watchdog is required after a "reset halt" in order to run
the CRC algorithm from RAM when verifying the chip. However, this is not
done automatically on probing or other initialization since disabling
the watchdog might interfere with debugging real applications.

The "mass_erase" command allows to erase the whole flash without
probing it, since in some scenarios the chip can be locked such that no
flash or ram can be accessed from the SWD interface, allowing only to
run a mass_erase to be able to flash the program.

The flashing process allows to compute a checksum, similar to the
lpc2000 driver "calc_checksum" but done over a different region of the
memory. This checksum is required to be present for the QN908x
bootloader ROM to boot, and otherwise is useless. As with the lpc2000
design, verification when using "calc_checksum" is expected to fail if
the checksum was not valid in the image being verified.

This was manually tested on a QN9080, including the scan-view,
AddressSanitizer/UBSan and test coverage configurations.

Change-Id: Ibd6d8f3608654294795085fcaaffb448b77cc58b
Co-developed-by: Marian Buschsieweke <marian.buschsieweke@ovgu.de>
Signed-off-by: Marian Buschsieweke <marian.buschsieweke@ovgu.de>
Signed-off-by: iosabi <iosabi@protonmail.com>
Reviewed-on: https://review.openocd.org/c/openocd/+/5584
Tested-by: jenkins
Reviewed-by: Antonio Borneo <borneo.antonio@gmail.com>
Reviewed-by: Tomas Vanek <vanekt@fbl.cz>
doc/openocd.texi
src/flash/nor/Makefile.am
src/flash/nor/driver.h
src/flash/nor/drivers.c
src/flash/nor/qn908x.c [new file with mode: 0644]
tcl/target/qn908x.cfg [new file with mode: 0644]

index 9b485c5e1f434f92d85328ef9ee16d36346bcdc4..832047f0ea63e5503ebe3e19e6d733c1f2fc284b 100644 (file)
@@ -7341,6 +7341,116 @@ Note: only Main and Work flash regions support Erase operation.
 @end deffn
 @end deffn
 
+@deffn {Flash Driver} {qn908x}
+The NXP QN908x microcontrollers feature a Cortex-M4F with integrated Bluetooth
+LE 5 support and an internal flash of up to 512 KiB. These chips only support
+the SWD interface.
+
+The @var{qn908x} driver uses the internal "Flash Memory Controller" block via
+SWD to erase, program and read the internal flash. This driver does not
+support the ISP (In-System Programming) mode which is an alternate way to
+program the flash via UART, SPI or USB.
+
+The internal flash is 512 KiB in size in all released chips and it starts at
+the address 0x01000000, although it can be mapped to address 0 and it is
+aliased to other addresses. This driver only recognizes the bank starting at
+address 0x01000000.
+
+The internal bootloader stored in ROM is in charge of loading and verifying
+the image from flash, or enter ISP mode. The programmed image must start at
+the beginning of the flash and contain a valid header and a matching CRC32
+checksum. Additionally, the image header contains a "Code Read Protection"
+(CRP) word which indicates whether SWD access is enabled, as well as whether
+ISP mode is enabled. Therefore, it is possible to program an image that
+disables SWD and ISP making it impossible to program another image in the
+future through these interfaces, or even debug the current image. While this is
+a valid use case for production deployments where the chips are locked down, by
+default this driver doesn't allow such images that disable the SWD interface.
+To program such images see the @command{qn908x allow_brick} command.
+
+Apart from the CRP field which is located in the image header, the last page
+of the flash memory contains a "Flash lock and protect" descriptor which allows
+to individually protect each 2 KiB page, as well as disabling SWD access to the
+flash and RAM. If this access is disabled it is not possible to read, erase or
+program individual pages from the SWD interface or even access the read-only
+"Flash information page" with information about the bootloader version and
+flash size. However when this protection is in place, it is still possible to
+mass erase the whole chip and then program a new image, for which you can use
+the @command{qn908x mass_erase}.
+
+Example:
+@example
+flash bank $FLASHNAME qn908x 0x01000000 0 0 0 $TARGETNAME calc_checksum
+@end example
+
+Parameters:
+@itemize
+@item @option{calc_checksum} optional parameter to compute the required
+checksum of the first bytes in the vector table.
+@quotation Note
+If the checksum in the header of your image is invalid and you don't provide the
+@option{calc_checksum} option the boot ROM will not boot your image and it may
+render the flash inaccessible. On the other hand, if you use this option to
+compute the checksum keep in mind that @command{verify_image} will fail on
+those four bytes of the checksum since those bytes in the flash will have the
+updated checksum.
+@end quotation
+@end itemize
+
+@deffn {Command} {qn908x allow_brick}
+Allow the qn908x driver to program images with a "Code Read Protection" byte
+that disables the SWD access. Programming such image will cause OpenOCD to
+not be able to reach the target over SWD anymore after the new image is
+programmed and its configuration takes effect, e.g. after a reboot. After
+executing @command{qn908x allow_brick} these images will be allowed to be
+programmed when writing to the flash.
+@end deffn
+
+@deffn {Command} {qn908x disable_wdog}
+Disable the watchdog timer (WDT) by resetting its CTRL field. The WDT starts
+enabled after a @command{reset halt} and it doesn't run while the target is
+halted. However, the verification process in this driver uses the generic
+Cortex-M verification process which executes a payload in RAM and thus
+requires the watchdog to be disabled before running @command{verify_image}
+after a reset halt or any other condition where the watchdog is running.
+Note that this is not done automatically and you must run this command in
+those scenarios.
+@end deffn
+
+@deffn {Command} {qn908x mass_erase}
+Erases the complete flash using the mass_erase method. Mass erase is only
+allowed if enabled in the Lock Status Register 8 (LOCK_STAT_8) which is read
+from the last sector of the flash on boot. However, this mass_erase lock
+protection can be bypassed and this command does so automatically.
+
+In the same LOCK_STAT_8 the flash and RAM access from SWD can be disabled by
+setting two bits in this register. After a mass_erase, all the bits of the
+flash would be set, making it the default to restrict SWD access to the flash
+and RAM regions. This new after erase LOCK_STAT_8 value only takes effect after
+being read from flash on the next reboot for example. After a mass_erase the
+LOCK_STAT_8 register is changed by the hardware to allow access to flash and
+RAM regardless of the value on flash, but only right after a mass_erase and
+until the next boot. Therefore it is possible to perform a mass_erase, program
+a new image, verify it and then reboot to a valid image that's locked from the
+SWD access.
+
+The @command{qn908x mass_erase} command clears the bits that would be loaded
+from the flash into LOCK_STAT_8 after erasing the whole chip to allow SWD
+access for debugging or re-flashing an image without a mass_erase by default.
+If the image being programmed also programs the last page of the flash with its
+own settings, this mass_erase behavior will interfere with that write since a
+new erase of at least the last page would need to be performed before writing
+to it again. For this reason the optional @option{keep_lock} argument can be
+used to leave the flash and RAM lock set. For development environments, the
+default behavior is desired.
+
+The mass erase locking mechanism is independent from the individual page
+locking bits, so it is possible that you can't erase a given page that is
+locked and you can't unprotect that page because the locking bits are also
+locked, but can still mass erase the whole flash.
+@end deffn
+@end deffn
+
 @deffn {Flash Driver} {rp2040}
 Supports RP2040 "Raspberry Pi Pico" microcontroller.
 RP2040 is a dual-core device with two CM0+ cores. Both cores share the same
index f04f0d206a76ca2417b934ad55c19c145431b53e..534a7a804e4a11f8c5f11217c6ac92eb1a106241 100644 (file)
@@ -54,6 +54,7 @@ NOR_DRIVERS = \
        %D%/psoc4.c \
        %D%/psoc5lp.c \
        %D%/psoc6.c \
+       %D%/qn908x.c \
        %D%/renesas_rpchf.c \
        %D%/rp2040.c \
        %D%/rsl10.c \
index 889a811e32faf913abde7794abea65e3eeb685eb..a63b72c8fa0bb7b1bee5bebf216b1759e0e44ec9 100644 (file)
@@ -284,6 +284,7 @@ extern const struct flash_driver psoc5lp_eeprom_flash;
 extern const struct flash_driver psoc5lp_flash;
 extern const struct flash_driver psoc5lp_nvl_flash;
 extern const struct flash_driver psoc6_flash;
+extern const struct flash_driver qn908x_flash;
 extern const struct flash_driver renesas_rpchf_flash;
 extern const struct flash_driver rp2040_flash;
 extern const struct flash_driver rsl10_flash;
index 92476987e44f5ec2bb33cd2cfac3da78746017af..3157bd3292e72831ea97a72e44e9dd7d08dd5b0d 100644 (file)
@@ -61,6 +61,7 @@ static const struct flash_driver * const flash_drivers[] = {
        &psoc5lp_eeprom_flash,
        &psoc5lp_nvl_flash,
        &psoc6_flash,
+       &qn908x_flash,
        &renesas_rpchf_flash,
        &rp2040_flash,
        &sh_qspi_flash,
diff --git a/src/flash/nor/qn908x.c b/src/flash/nor/qn908x.c
new file mode 100644 (file)
index 0000000..8cd7a2f
--- /dev/null
@@ -0,0 +1,1197 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/***************************************************************************
+ *   Copyright (C) 2020 iosabi                                             *
+ *   iosabi <iosabi@protonmail.com>                                        *
+ ***************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "imp.h"
+
+#include <helper/binarybuffer.h>
+#include <helper/bits.h>
+#include <helper/crc32.h>
+#include <helper/time_support.h>
+#include <helper/types.h>
+
+/* The QN908x has two flash regions, one is the main flash region holding the
+ * user code and the second one is a small (0x800 bytes) "Flash information
+ * page" that can't be written to by the user. This page contains information
+ * programmed at the factory.
+ *
+ * The main flash region is normally 512 KiB, there's a field in the "Flash
+ * information page" that allows to specify for 256 KiB size chips. However, at
+ * the time of writing, none of the variants in the market have 256 KiB.
+ *
+ * The flash is divided into blocks of 256 KiB each, therefore containing two
+ * blocks. A block is subdivided into pages, of 2048 bytes. A page is the
+ * smallest region that can be erased or protected independently, although it
+ * is also possible to erase a whole block or both blocks. A page is subdivided
+ * into 8 rows of 64 words (32-bit words). The word subdivision is only
+ * relevant because DMA can write multiple words in the same row in the same
+ * flash program operation.
+ *
+ * For the Flash information page we are only interested in the last
+ * 0x100 bytes which contain a CRC-32 checksum of that 0x100 bytes long region
+ * and a field stating the size of the flash. This is also a good check that
+ * we are dealing with the right chip/flash configuration and is used in the
+ * probe() function.
+ */
+#define QN908X_FLASH_BASE                                              0x01000000
+
+#define QN908X_FLASH_PAGE_SIZE                                 2048
+#define QN908X_FLASH_PAGES_PER_BLOCK                   128
+#define QN908X_FLASH_MAX_BLOCKS                                        2
+#define QN908X_FLASH_BLOCK_SIZE \
+               (QN908X_FLASH_PAGES_PER_BLOCK * QN908X_FLASH_PAGE_SIZE)
+#define QN908X_FLASH_IRQ_VECTOR_CHECKSUM_POS   0x1c
+#define QN908X_FLASH_IRQ_VECTOR_CHECKSUM_SIZE  4
+#define QN908X_FLASH_IRQ_VECTOR_CHECKSUM_END   \
+       (QN908X_FLASH_IRQ_VECTOR_CHECKSUM_POS + QN908X_FLASH_IRQ_VECTOR_CHECKSUM_SIZE)
+
+
+/* Flash information page memory fields. */
+#define QN908X_INFO_PAGE_BASE                                  0x210b0000u
+#define QN908X_INFO_PAGE_CRC32                                 (QN908X_INFO_PAGE_BASE + 0x700)
+#define QN908X_INFO_PAGE_CRC_START                             (QN908X_INFO_PAGE_BASE + 0x704)
+#define QN908X_INFO_PAGE_BOOTLOADER_VER                        (QN908X_INFO_PAGE_BASE + 0x704)
+#define QN908X_INFO_PAGE_FLASH_SIZE                            (QN908X_INFO_PAGE_BASE + 0x708)
+#define QN908X_INFO_PAGE_BLUETOOTH_ADDR                        (QN908X_INFO_PAGE_BASE + 0x7fa)
+#define QN908X_INFO_PAGE_CRC_END                               (QN908X_INFO_PAGE_BASE + 0x800)
+
+
+/* Possible values of the QN908X_INFO_PAGE_FLASH_SIZE field. */
+enum qn908x_info_page_flash_size {
+       QN908X_FLASH_SIZE_512K                                  = 0xfffff0ff,
+       QN908X_FLASH_SIZE_256K                                  = 0xffffe0ff,
+};
+
+/* QN908x "Flash memory controller", described in section 28 of the user
+ * manual. In the NXP SDK this peripheral is called "FLASH", however we use the
+ * name "FMC" (Flash Memory Controller) here when referring to the controller
+ * to avoid confusion with other "flash" terms in OpenOCD. */
+#define QN908X_FMC_BASE 0x40081000u
+#define QN908X_FMC_INI_RD_EN                                           (QN908X_FMC_BASE + 0x00)
+#define QN908X_FMC_ERASE_CTRL                                          (QN908X_FMC_BASE + 0x04)
+#define QN908X_FMC_ERASE_TIME                                          (QN908X_FMC_BASE + 0x08)
+#define QN908X_FMC_TIME_CTRL                                           (QN908X_FMC_BASE + 0x0c)
+#define QN908X_FMC_SMART_CTRL                                          (QN908X_FMC_BASE + 0x10)
+#define QN908X_FMC_INT_STAT                                                    (QN908X_FMC_BASE + 0x18)
+#define QN908X_FMC_LOCK_STAT_0                                         (QN908X_FMC_BASE + 0x20)
+#define QN908X_FMC_LOCK_STAT_1                                         (QN908X_FMC_BASE + 0x24)
+#define QN908X_FMC_LOCK_STAT_2                                         (QN908X_FMC_BASE + 0x28)
+#define QN908X_FMC_LOCK_STAT_3                                         (QN908X_FMC_BASE + 0x2c)
+#define QN908X_FMC_LOCK_STAT_4                                         (QN908X_FMC_BASE + 0x30)
+#define QN908X_FMC_LOCK_STAT_5                                         (QN908X_FMC_BASE + 0x34)
+#define QN908X_FMC_LOCK_STAT_6                                         (QN908X_FMC_BASE + 0x38)
+#define QN908X_FMC_LOCK_STAT_7                                         (QN908X_FMC_BASE + 0x3c)
+#define QN908X_FMC_LOCK_STAT_8                                         (QN908X_FMC_BASE + 0x40)
+#define QN908X_FMC_STATUS1                                                     (QN908X_FMC_BASE + 0x48)
+#define QN908X_FMC_DEBUG_PASSWORD                                      (QN908X_FMC_BASE + 0xa8)
+#define QN908X_FMC_ERASE_PASSWORD                                      (QN908X_FMC_BASE + 0xac)
+
+#define QN908X_FMC_INI_RD_EN_INI_RD_EN_MASK                    BIT(0)
+
+#define QN908X_FMC_STATUS1_FSH_ERA_BUSY_L_MASK         BIT(9)
+#define QN908X_FMC_STATUS1_FSH_WR_BUSY_L_MASK          BIT(10)
+#define QN908X_FMC_STATUS1_FSH_ERA_BUSY_H_MASK         BIT(12)
+#define QN908X_FMC_STATUS1_FSH_WR_BUSY_H_MASK          BIT(13)
+#define QN908X_FMC_STATUS1_INI_RD_DONE_MASK                    BIT(15)
+#define QN908X_FMC_STATUS1_FSH_STA_MASK                                BIT(26)
+
+#define QN908X_FMC_ERASE_CTRL_PAGE_IDXL_SHIFT          0
+#define QN908X_FMC_ERASE_CTRL_PAGE_IDXH_SHIFT          8
+#define QN908X_FMC_ERASE_CTRL_HALF_ERASEL_EN_SHIFT     28
+#define QN908X_FMC_ERASE_CTRL_HALF_ERASEH_EN_SHIFT     29
+#define QN908X_FMC_ERASE_CTRL_PAGE_ERASEL_EN_SHIFT     30
+#define QN908X_FMC_ERASE_CTRL_PAGE_ERASEH_EN_SHIFT     31
+
+#define QN908X_FMC_INT_STAT_AHBL_INT_MASK                      BIT(0)
+#define QN908X_FMC_INT_STAT_LOCKL_INT_MASK                     BIT(1)
+#define QN908X_FMC_INT_STAT_ERASEL_INT_MASK                    BIT(2)
+#define QN908X_FMC_INT_STAT_WRITEL_INT_MASK                    BIT(3)
+#define QN908X_FMC_INT_STAT_WR_BUFL_INT_MASK           BIT(4)
+#define QN908X_FMC_INT_STAT_WRITE_FAIL_L_INT_MASK      BIT(5)
+#define QN908X_FMC_INT_STAT_ERASE_FAIL_L_INT_MASK      BIT(6)
+#define QN908X_FMC_INT_STAT_AHBH_INT_MASK                      BIT(8)
+#define QN908X_FMC_INT_STAT_LOCKH_INT_MASK                     BIT(9)
+#define QN908X_FMC_INT_STAT_ERASEH_INT_MASK                    BIT(10)
+#define QN908X_FMC_INT_STAT_WRITEH_INT_MASK                    BIT(11)
+#define QN908X_FMC_INT_STAT_WR_BUFH_INT_MASK           BIT(12)
+#define QN908X_FMC_INT_STAT_WRITE_FAIL_H_INT_MASK      BIT(13)
+#define QN908X_FMC_INT_STAT_ERASE_FAIL_H_INT_MASK      BIT(14)
+
+#define QN908X_FMC_SMART_CTRL_PRGML_EN_MASK                    BIT(0)
+#define QN908X_FMC_SMART_CTRL_PRGMH_EN_MASK                    BIT(1)
+#define QN908X_FMC_SMART_CTRL_SMART_WRITEL_EN_MASK     BIT(2)
+#define QN908X_FMC_SMART_CTRL_SMART_WRITEH_EN_MASK     BIT(3)
+#define QN908X_FMC_SMART_CTRL_SMART_ERASEL_EN_MASK     BIT(4)
+#define QN908X_FMC_SMART_CTRL_SMART_ERASEH_EN_MASK     BIT(5)
+#define QN908X_FMC_SMART_CTRL_MAX_WRITE_MASK           0xf00u
+#define QN908X_FMC_SMART_CTRL_MAX_WRITE_SHIFT          8u
+#define QN908X_FMC_SMART_CTRL_MAX_WRITE(x) \
+       (((uint32_t)(((uint32_t)(x)) << QN908X_FMC_SMART_CTRL_MAX_WRITE_SHIFT)) \
+       & QN908X_FMC_SMART_CTRL_MAX_WRITE_MASK)
+#define QN908X_FMC_SMART_CTRL_MAX_ERASE_MASK           0x3f000u
+#define QN908X_FMC_SMART_CTRL_MAX_ERASE_SHIFT          12u
+#define QN908X_FMC_SMART_CTRL_MAX_ERASE(x) \
+       (((uint32_t)(((uint32_t)(x)) << QN908X_FMC_SMART_CTRL_MAX_ERASE_SHIFT)) \
+       & QN908X_FMC_SMART_CTRL_MAX_ERASE_MASK)
+
+#define QN908X_FMC_SMART_CTRL_MAX_ERASE_RETRIES                9
+#define QN908X_FMC_SMART_CTRL_MAX_WRITE_RETRIES                9
+
+#define QN908X_FMC_TIME_CTRL_PRGM_CYCLE_MASK           0xfffu
+#define QN908X_FMC_TIME_CTRL_PRGM_CYCLE_SHIFT          0u
+#define QN908X_FMC_TIME_CTRL_PRGM_CYCLE(x) \
+       (((uint32_t)(((uint32_t)(x)) << QN908X_FMC_TIME_CTRL_PRGM_CYCLE_SHIFT)) \
+       & QN908X_FMC_TIME_CTRL_PRGM_CYCLE_MASK)
+#define QN908X_FMC_TIME_CTRL_TIME_BASE_MASK                    0xff000u
+#define QN908X_FMC_TIME_CTRL_TIME_BASE_SHIFT           12u
+#define QN908X_FMC_TIME_CTRL_TIME_BASE(x) \
+       (((uint32_t)(((uint32_t)(x)) << QN908X_FMC_TIME_CTRL_TIME_BASE_SHIFT)) \
+       & QN908X_FMC_TIME_CTRL_TIME_BASE_MASK)
+
+#define QN908X_FMC_LOCK_STAT_8_MASS_ERASE_LOCK_EN      BIT(0)
+#define QN908X_FMC_LOCK_STAT_8_FSH_PROTECT_EN          BIT(1)
+#define QN908X_FMC_LOCK_STAT_8_MEM_PROTECT_EN          BIT(2)
+#define QN908X_FMC_LOCK_STAT_8_PROTECT_ANY                     (BIT(1) | BIT(2))
+
+/* See Table 418 "Flash lock and protect description" in the user manual */
+#define QN908X_FLASH_LOCK_ADDR                                         (QN908X_FLASH_BASE + 0x7f820)
+/* Allow mass erase */
+#define QN908X_FLASH_LOCK_ENABLE_MASS_ERASE                    BIT(0)
+/* disallow flash access from SWD */
+#define QN908X_FLASH_LOCK_ENABLE_FLASH_PROTECTION      BIT(1)
+/* disallow SRAM access from SWD */
+#define QN908X_FLASH_LOCK_ENABLE_MEMORY_PROTECTION     BIT(2)
+
+/* Page lock information located at the beginning of the last page. */
+struct qn908x_flash_page_lock {
+       uint8_t bits[QN908X_FLASH_MAX_BLOCKS * QN908X_FLASH_PAGES_PER_BLOCK / 8];
+       uint8_t protection;
+       uint8_t _reserved[3];
+       /* nvds_size is unused here, but we need to preserve it across erases
+        * when locking and unlocking pages. */
+       uint8_t nvds_size[4];
+} __attribute__ ((packed));
+
+/* Clock configuration is stored in the SYSCON. */
+#define QN908X_SYSCON_BASE                                             0x40000000u
+#define QN908X_SYSCON_CLK_EN                                   (QN908X_SYSCON_BASE + 0x00cu)
+#define QN908X_SYSCON_CLK_CTRL                                 (QN908X_SYSCON_BASE + 0x010u)
+#define QN908X_SYSCON_CHIP_ID                                  (QN908X_SYSCON_BASE + 0x108u)
+#define QN908X_SYSCON_XTAL_CTRL                                        (QN908X_SYSCON_BASE + 0x180u)
+
+/* Internal 16MHz / 8MHz clock used by the erase operation. */
+#define QN908X_SYSCON_CLK_EN_CLK_DP_EN_MASK            BIT(21)
+
+#define SYSCON_XTAL_CTRL_XTAL_DIV_MASK                 BIT(31)
+
+#define SYSCON_CLK_CTRL_AHB_DIV_MASK                   0x1FFF0u
+#define SYSCON_CLK_CTRL_AHB_DIV_SHIFT                  4u
+#define SYSCON_CLK_CTRL_CLK_XTAL_SEL_MASK              BIT(19)
+#define SYSCON_CLK_CTRL_CLK_OSC32M_DIV_MASK            BIT(20)
+#define SYSCON_CLK_CTRL_SYS_CLK_SEL_MASK               0xC0000000u
+#define SYSCON_CLK_CTRL_SYS_CLK_SEL_SHIFT              30u
+
+#define CLOCK_16MHZ                                                            16000000u
+#define CLOCK_32MHZ                                                            32000000u
+#define CLOCK_32KHZ                                                            32000u
+
+/* Watchdog block registers */
+#define QN908X_WDT_BASE                                                        0x40001000u
+#define QN908X_WDT_CTRL                                                        (QN908X_WDT_BASE + 0x08u)
+#define QN908X_WDT_LOCK                                                        (QN908X_WDT_BASE + 0x20u)
+
+struct qn908x_flash_bank {
+       /* The number of flash blocks. Initially set to zero until the flash
+        * is probed. This determines the size of the flash. */
+       unsigned int num_blocks;
+
+       unsigned int user_bank_size;
+       bool calc_checksum;
+
+       /* Whether we allow to flash an image that disables SWD access, potentially
+        * bricking the device since the image can't be reflashed from SWD. */
+       bool allow_swd_disabled;
+
+       bool page_lock_loaded;
+       struct qn908x_flash_page_lock page_lock;
+};
+
+/* 500 ms timeout. */
+#define QN908X_DEFAULT_TIMEOUT_MS 500
+
+/* Forward declaration of commands. */
+static int qn908x_probe(struct flash_bank *bank);
+static int qn908x_write(struct flash_bank *bank, const uint8_t *buffer,
+               uint32_t offset, uint32_t count);
+
+/* Update the value of a register with a mask. This helper allows to read a
+ * register, modify a subset of the bits and write back the value, which is a
+ * common operation when modifying only a bit filed in a register. */
+static int qn908x_update_reg(struct target *target, target_addr_t reg,
+               uint32_t mask, uint32_t value)
+{
+       uint32_t orig_value = 0;
+       uint32_t new_value;
+       int retval;
+       if (mask != 0xffffffff) {
+               /* No need to read the old value if we request a mask of 32 bits. */
+               retval = target_read_u32(target, reg, &orig_value);
+               if (retval != ERROR_OK) {
+                       LOG_DEBUG("Error reading reg at " TARGET_ADDR_FMT
+                                       ": %d", reg, retval);
+                       return retval;
+               }
+       }
+       new_value = (orig_value & ~mask) | (value & mask);
+       retval = target_write_u32(target, reg, new_value);
+       if (retval != ERROR_OK) {
+               LOG_DEBUG("Error writing reg at " TARGET_ADDR_FMT " with 0x%08"
+                               PRIx32 ": %d", reg, new_value, retval);
+               return retval;
+       }
+       if (mask == 0xffffffff) {
+               LOG_DEBUG("Updated reg at " TARGET_ADDR_FMT ": ?? -> 0x%.08"
+                               PRIx32 "", reg, new_value);
+       } else {
+               LOG_DEBUG("Updated reg at " TARGET_ADDR_FMT ": 0x%.08" PRIx32
+                               " -> 0x%.08" PRIx32, reg, orig_value, new_value);
+       }
+       return ERROR_OK;
+}
+
+/* Load lock bit and protection bit and load redundancy page info.
+ * This populates the LOCK_STAT_n registers with the values from the lock page,
+ * making protection bit changes to the last page effective. */
+static int qn908x_load_lock_stat(struct target *target)
+{
+       int retval = target_write_u32(target, QN908X_FMC_INI_RD_EN,
+                       QN908X_FMC_INI_RD_EN_INI_RD_EN_MASK);
+       if (retval != ERROR_OK)
+               return retval;
+
+       uint32_t status1;
+       const uint32_t status_mask = QN908X_FMC_STATUS1_FSH_STA_MASK
+                       | QN908X_FMC_STATUS1_INI_RD_DONE_MASK;
+       do {
+               retval = target_read_u32(target, QN908X_FMC_STATUS1, &status1);
+               if (retval != ERROR_OK)
+                       return retval;
+       } while ((status1 & status_mask) != QN908X_FMC_STATUS1_INI_RD_DONE_MASK);
+
+       for (int i = 0; i <= 8; i++) {
+               uint32_t addr = QN908X_FMC_LOCK_STAT_0 + i * 4;
+               uint32_t lock_stat;
+               if (target_read_u32(target, addr, &lock_stat) == ERROR_OK)
+                       LOG_DEBUG("LOCK_STAT_%d = 0x%08" PRIx32, i, lock_stat);
+       }
+       return ERROR_OK;
+}
+
+/* Initializes the FMC controller registers for allowing writing. */
+static int qn908x_init_flash(struct target *target)
+{
+       /* Determine the current clock configuration. */
+       uint32_t clk_ctrl;
+       int retval = target_read_u32(target, QN908X_SYSCON_CLK_CTRL, &clk_ctrl);
+       if (retval != ERROR_OK)
+               return retval;
+
+       uint32_t clk_sel = (clk_ctrl & SYSCON_CLK_CTRL_SYS_CLK_SEL_MASK)
+                       >> SYSCON_CLK_CTRL_SYS_CLK_SEL_SHIFT;
+       LOG_DEBUG("Clock clk_sel=0x%08" PRIu32, clk_sel);
+
+       /* Core clock frequency. */
+       uint32_t core_freq = 0;
+       switch (clk_sel) {
+       case 0: /* RCO 32 MHz */
+               core_freq = (clk_ctrl & SYSCON_CLK_CTRL_CLK_OSC32M_DIV_MASK) ?
+                       CLOCK_16MHZ : CLOCK_32MHZ;
+               break;
+       case 1: /* Xin frequency */
+       {
+               uint32_t clk_xtal;
+               retval = target_read_u32(target, QN908X_SYSCON_XTAL_CTRL, &clk_xtal);
+               if (retval != ERROR_OK)
+                       return retval;
+               core_freq       = (clk_ctrl & SYSCON_CLK_CTRL_CLK_XTAL_SEL_MASK)
+                                       && (clk_xtal & SYSCON_XTAL_CTRL_XTAL_DIV_MASK)
+                                       ? CLOCK_32MHZ : CLOCK_16MHZ;
+       }
+       break;
+       case 2: /* 32 Kz */
+               core_freq = CLOCK_32KHZ;
+               break;
+       default:
+               return ERROR_FAIL;
+       }
+
+       uint32_t ahb_div = (clk_ctrl & SYSCON_CLK_CTRL_AHB_DIV_MASK)
+                       >> SYSCON_CLK_CTRL_AHB_DIV_SHIFT;
+       uint32_t ahb_freq = core_freq / (ahb_div + 1);
+
+       LOG_DEBUG("Core freq: %" PRIu32 " Hz | AHB freq: %" PRIu32 " Hz",
+                       core_freq, ahb_freq);
+
+       /* TIME_BASE is 2uS at the current AHB clock speed. */
+       retval = target_write_u32(target, QN908X_FMC_TIME_CTRL,
+                       QN908X_FMC_TIME_CTRL_TIME_BASE(2 * ahb_freq / 1000000) |
+                       QN908X_FMC_TIME_CTRL_PRGM_CYCLE(30));
+       if (retval != ERROR_OK)
+               return retval;
+
+       return qn908x_load_lock_stat(target);
+}
+
+/* flash bank qn908x <base> <size> 0 0 <target#> [calc_checksum] */
+FLASH_BANK_COMMAND_HANDLER(qn908x_flash_bank_command)
+{
+       struct qn908x_flash_bank *qn908x_info;
+
+       if (CMD_ARGC < 6 || CMD_ARGC > 7)
+               return ERROR_COMMAND_SYNTAX_ERROR;
+
+       if (bank->base != QN908X_FLASH_BASE) {
+               LOG_ERROR("Address " TARGET_ADDR_FMT
+                       " is an invalid bank address (try 0x%08" PRIx32 ")",
+                       bank->base, QN908X_FLASH_BASE);
+               return ERROR_COMMAND_ARGUMENT_INVALID;
+       }
+
+       qn908x_info = malloc(sizeof(struct qn908x_flash_bank));
+
+       if (!qn908x_info)
+               return ERROR_FAIL;
+
+       bank->driver_priv = qn908x_info;
+       qn908x_info->num_blocks = 0;
+       qn908x_info->user_bank_size = bank->size;
+       qn908x_info->page_lock_loaded = false;
+       qn908x_info->allow_swd_disabled = false;
+
+       qn908x_info->calc_checksum = false;
+       if (CMD_ARGC == 7) {
+               if (strcmp(CMD_ARGV[6], "calc_checksum")) {
+                       free(qn908x_info);
+                       return ERROR_COMMAND_ARGUMENT_INVALID;
+               }
+               qn908x_info->calc_checksum = true;
+       }
+
+       return ERROR_OK;
+}
+
+static int qn908x_read_page_lock(struct flash_bank *bank)
+{
+       struct qn908x_flash_bank *qn908x_info = bank->driver_priv;
+
+       if (bank->target->state != TARGET_HALTED) {
+               LOG_ERROR("Target not halted");
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       /* The last page of the flash contains the "Flash lock and protect"
+        * information. It is not clear where this is located on chips with only
+        * one block. */
+       uint32_t prot_offset = qn908x_info->num_blocks * QN908X_FLASH_BLOCK_SIZE
+                       - QN908X_FLASH_PAGE_SIZE;
+
+       int retval = target_read_memory(bank->target, bank->base + prot_offset, 4,
+                       sizeof(qn908x_info->page_lock) / 4,
+                       (void *)(&qn908x_info->page_lock));
+       if (retval != ERROR_OK)
+               return retval;
+       LOG_DEBUG("Flash protection = 0x%02" PRIx8,
+                       qn908x_info->page_lock.protection);
+
+       qn908x_info->page_lock_loaded = true;
+       return ERROR_OK;
+}
+
+static int qn908x_busy_check(struct target *target)
+{
+       uint32_t status1;
+       int retval = target_read_u32(target, QN908X_FMC_STATUS1, &status1);
+       if (retval != ERROR_OK)
+               return retval;
+
+       if ((status1 & (QN908X_FMC_STATUS1_FSH_ERA_BUSY_L_MASK
+                                       | QN908X_FMC_STATUS1_FSH_WR_BUSY_L_MASK
+                                       | QN908X_FMC_STATUS1_FSH_ERA_BUSY_H_MASK
+                                       | QN908X_FMC_STATUS1_FSH_WR_BUSY_H_MASK)))
+               return ERROR_FLASH_BUSY;
+       return ERROR_OK;
+}
+
+static int qn908x_status_check(struct target *target)
+{
+       uint32_t int_stat;
+       int retval = target_read_u32(target, QN908X_FMC_INT_STAT, &int_stat);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* The error bits for block 0 and block 1 have the exact same layout, only
+        * that block 1 error bits are shifted by 8 bits. We use this fact to
+        * loop over the blocks */
+       for (unsigned int block = 0; block <= 1; block++) {
+               unsigned int shift = (block) ? 8 : 0;
+               if (int_stat & (QN908X_FMC_INT_STAT_AHBL_INT_MASK << shift)) {
+                       LOG_ERROR("AHB error on block %u", block);
+                       return ERROR_FAIL;
+               }
+
+               if (int_stat & (QN908X_FMC_INT_STAT_LOCKL_INT_MASK << shift)) {
+                       LOG_ERROR("Locked page being accessed error on block %u", block);
+                       return ERROR_FAIL;
+               }
+
+               if (int_stat & (QN908X_FMC_INT_STAT_WRITE_FAIL_L_INT_MASK << shift)) {
+                       LOG_ERROR("Smart write on block %u failed", block);
+                       return ERROR_FAIL;
+               }
+
+               if ((int_stat & (QN908X_FMC_INT_STAT_ERASE_FAIL_L_INT_MASK << shift))
+                               || (int_stat & (QN908X_FMC_INT_STAT_ERASE_FAIL_H_INT_MASK << shift))) {
+                       LOG_ERROR("Smart erase on block %u failed", block);
+                       return ERROR_FAIL;
+               }
+       }
+
+       return ERROR_OK;
+}
+
+static int qn908x_wait_for_idle(struct target *target, int64_t timeout_ms)
+{
+       int64_t ms_start = timeval_ms();
+
+       int busy = ERROR_FLASH_BUSY;
+       while (busy != ERROR_OK) {
+               busy = qn908x_busy_check(target);
+               if (busy != ERROR_OK && busy != ERROR_FLASH_BUSY)
+                       return busy;
+               if (timeval_ms() - ms_start > timeout_ms) {
+                       LOG_ERROR("Timeout waiting to be idle.");
+                       return ERROR_TIMEOUT_REACHED;
+               }
+       }
+       return ERROR_OK;
+}
+
+/* Set up the chip to perform an erase (page or block) operation. */
+static int qn908x_setup_erase(struct target *target)
+{
+       int retval;
+       if (target->state != TARGET_HALTED) {
+               LOG_ERROR("Target not halted");
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       /* Enable 8MHz clock. */
+       retval = qn908x_update_reg(target, QN908X_SYSCON_CLK_EN,
+                       QN908X_SYSCON_CLK_EN_CLK_DP_EN_MASK,
+                       QN908X_SYSCON_CLK_EN_CLK_DP_EN_MASK);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set ERASE_TIME to 2ms for smart erase. */
+       retval = qn908x_update_reg(target, QN908X_FMC_ERASE_TIME,
+                       (1u << 20) - 1,
+                       2000 * 8);      /* 2000 uS * 8 MHz = x cycles */
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set up smart erase. SWD can only perform smart erase. */
+       uint32_t ctrl_val       = QN908X_FMC_SMART_CTRL_SMART_ERASEH_EN_MASK
+                                               | QN908X_FMC_SMART_CTRL_SMART_ERASEL_EN_MASK
+                                               | QN908X_FMC_SMART_CTRL_MAX_ERASE(QN908X_FMC_SMART_CTRL_MAX_ERASE_RETRIES)
+                                               | QN908X_FMC_SMART_CTRL_MAX_WRITE(QN908X_FMC_SMART_CTRL_MAX_WRITE_RETRIES);
+       retval = target_write_u32(target, QN908X_FMC_SMART_CTRL, ctrl_val);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = qn908x_wait_for_idle(target, QN908X_DEFAULT_TIMEOUT_MS);
+       if (retval != ERROR_OK)
+               return retval;
+
+       return ERROR_OK;
+}
+
+static int qn908x_erase(struct flash_bank *bank, unsigned int first,
+               unsigned int last)
+{
+       struct qn908x_flash_bank *qn908x_info = bank->driver_priv;
+       int retval = ERROR_OK;
+
+       if (!qn908x_info->num_blocks) {
+               if (qn908x_probe(bank) != ERROR_OK)
+                       return ERROR_FLASH_BANK_NOT_PROBED;
+       }
+
+       retval = qn908x_setup_erase(bank->target);
+       if (retval != ERROR_OK)
+               return retval;
+
+       for (unsigned int i = first; i <= last; i++) {
+               if (i >= bank->num_sectors)
+                       return ERROR_FLASH_SECTOR_INVALID;
+               uint32_t block_idx = i / QN908X_FLASH_PAGES_PER_BLOCK;
+               uint32_t page_idx = i % QN908X_FLASH_PAGES_PER_BLOCK;
+               if (block_idx >= qn908x_info->num_blocks)
+                       return ERROR_FLASH_SECTOR_INVALID;
+
+               LOG_DEBUG("Erasing page %" PRIu32 " of block %" PRIu32,
+                       page_idx, block_idx);
+
+               /* Depending on the block the page we are erasing is located we
+                * need to use a different set of bits in the registers. */
+               uint32_t ctrl_page_idx_shift = block_idx ?
+                       QN908X_FMC_ERASE_CTRL_PAGE_IDXH_SHIFT :
+                       QN908X_FMC_ERASE_CTRL_PAGE_IDXL_SHIFT;
+               uint32_t ctrl_erase_en_shift = block_idx ?
+                       QN908X_FMC_ERASE_CTRL_PAGE_ERASEH_EN_SHIFT :
+                       QN908X_FMC_ERASE_CTRL_PAGE_ERASEL_EN_SHIFT;
+
+               retval = target_write_u32(bank->target, QN908X_FMC_ERASE_CTRL,
+                               BIT(ctrl_erase_en_shift) | (page_idx << ctrl_page_idx_shift));
+               if (retval != ERROR_OK)
+                       return retval;
+
+               retval = qn908x_wait_for_idle(bank->target, QN908X_DEFAULT_TIMEOUT_MS);
+               if (retval != ERROR_OK)
+                       return retval;
+
+               retval = qn908x_status_check(bank->target);
+               if (retval != ERROR_OK)
+                       return retval;
+       }
+
+       return retval;
+}
+
+static int qn908x_protect(struct flash_bank *bank, int set, unsigned int first,
+               unsigned int last)
+{
+       struct qn908x_flash_bank *qn908x_info = bank->driver_priv;
+
+       if (bank->target->state != TARGET_HALTED) {
+               LOG_ERROR("Target not halted");
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       if (!qn908x_info->page_lock_loaded) {
+               int retval = qn908x_read_page_lock(bank);
+               if (retval != ERROR_OK)
+                       return retval;
+       }
+
+       /* Use [first, last) interval open on the right side from now on. */
+       last++;
+       /* We use sectors as prot_blocks. */
+       bool needs_update = false;
+       for (unsigned int i = first; i < last; i++) {
+               if (set != (((qn908x_info->page_lock.bits[i / 8] >> (i % 8)) & 1) ^ 1))
+                       needs_update = true;
+       }
+
+       /* Check that flash protection still allows SWD access to flash and RAM,
+        * otherwise we won't be able to re-flash this chip from SWD unless we do a
+        * mass erase. */
+       if (qn908x_info->page_lock.protection & QN908X_FMC_LOCK_STAT_8_PROTECT_ANY) {
+               LOG_WARNING("SWD flash/RAM access disabled in the Flash lock and "
+                       "protect descriptor. You might need to issue a mass_erase to "
+                       "regain SWD access to this chip after reboot.");
+       }
+
+       if (!needs_update)
+               return ERROR_OK;
+
+       int last_page = qn908x_info->num_blocks * QN908X_FLASH_PAGES_PER_BLOCK - 1;
+       int retval;
+
+       if (qn908x_info->page_lock.bits[sizeof(qn908x_info->page_lock.bits) - 1] & 0x80) {
+               /* A bit 1 in the MSB in the page_lock.bits array means that the last
+                * page is unlocked, so we can just erase it. */
+               retval = qn908x_erase(bank, last_page, last_page);
+               if (retval != ERROR_OK)
+                       return retval;
+       } else {
+               /* TODO: The last page is locked and we can't erase unless we use the
+                * ERASE_PASSWORD from code running on the device. For this we need to
+                * copy a little program to RAM and execute the erase command from
+                * there since there's no way to override the page protection from
+                * SWD. */
+               LOG_ERROR("Unprotecting the last page is not supported. Issue a "
+                       "\"qn908x mass_erase\" command to erase the whole flash, "
+                       "including the last page and its protection.");
+               return ERROR_FAIL;
+       }
+
+       for (unsigned int i = first / 8; i < (last + 7) / 8; i++) {
+               /* first_mask contains a bit set if the bit corresponds to a block id
+                * that is larger or equal than first. This is basically 0xff in all
+                * cases except potentially the first iteration. */
+               uint8_t first_mask      = (first <= i * 8)
+                                                       ? 0xff : 0xff ^ ((1u << (first - i * 8)) - 1);
+               /* Similar to first_mask, this contains a bit set if the corresponding
+                * is smaller than last. */
+               uint8_t last_mask       = (i * 8 + 8 <= last)
+                                                       ? 0xff : ((1u << (last - i * 8)) - 1);
+
+               uint8_t mask = first_mask & last_mask;
+               LOG_DEBUG("protect set=%d bits[%d] with mask=0x%02x", set, i, mask);
+               /* To "set" the protection bit means to clear the bit in the page_lock
+                * bit array. */
+               if (set)
+                       qn908x_info->page_lock.bits[i] &= ~mask;
+               else
+                       qn908x_info->page_lock.bits[i] |= mask;
+       }
+
+       retval = qn908x_write(bank, (void *)(&qn908x_info->page_lock),
+                       last_page * QN908X_FLASH_PAGE_SIZE, sizeof(qn908x_info->page_lock));
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Reload the lock_stat to make the changes effective. */
+       retval = qn908x_load_lock_stat(bank->target);
+       if (retval != ERROR_OK)
+               return retval;
+
+       for (unsigned int i = first; i < last; i++)
+               bank->sectors[i].is_protected = set;
+
+       return ERROR_OK;
+}
+
+static int qn908x_write(struct flash_bank *bank, const uint8_t *buffer,
+               uint32_t offset, uint32_t count)
+{
+       struct qn908x_flash_bank *qn908x_info = bank->driver_priv;
+       int retval = ERROR_OK;
+
+       if (bank->target->state != TARGET_HALTED) {
+               LOG_ERROR("Target not halted");
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       /* The flash infrastructure was requested to align writes to 32 bit */
+       assert(((offset % 4) == 0) && ((count % 4) == 0));
+
+       /* Compute the calc_checksum even if it wasn't requested. */
+       uint32_t checksum = 0;
+       if (offset == 0 && count >= 0x20) {
+               for (int i = 0; i < 7; i++)
+                       checksum += buf_get_u32(buffer + (i * 4), 0, 32);
+               checksum = 0 - checksum;
+               LOG_DEBUG("computed image checksum: 0x%8.8" PRIx32, checksum);
+               uint32_t stored_checksum = buf_get_u32(buffer + 7 * 4, 0, 32);
+               if (checksum != stored_checksum) {
+                       LOG_WARNING("Image vector table checksum mismatch: expected 0x%08"
+                                       PRIx32 " but found 0x%08" PRIx32,
+                                       checksum, stored_checksum);
+                       if (!qn908x_info->calc_checksum)
+                               LOG_WARNING("This device will not boot, use calc_checksum in "
+                                               "the flash bank.");
+                       else
+                               LOG_WARNING("Updating checksum, verification will fail.");
+               }
+       }
+
+       /* Check the Code Read Protection (CRP) word for invalid values or not
+        * allowed ones. */
+       if (offset <= 0x20 && offset + count >= 0x24) {
+               uint32_t crp = buf_get_u32(buffer + 0x20 - offset, 0, 32);
+               /* 2-bit fields at bits 10, 12, 14, 16 and 18 must not be 00 or 11. */
+               for (int i = 10; i <= 18; i += 2) {
+                       uint32_t field = (crp >> i) & 3;
+                       if (field == 0 || field == 3) {
+                               LOG_DEBUG("Code Read Protection = 0x%08" PRIx32, crp);
+                               LOG_ERROR("The Code Read Protection (CRP) field at bit %d is "
+                                               "invalid (%" PRIu32 "). An invalid value could make "
+                                               "the flash inaccessible.", i, field);
+                               return ERROR_FAIL;
+                       }
+               }
+
+               uint32_t swd_allowed = (crp >> 18) & 3;
+               if (swd_allowed != 2) {
+                       LOG_WARNING("The Code Read Protection (CRP) in this image "
+                                       "(0x%08" PRIx32 ") is disabling the SWD access, which is "
+                                       "currently used by OpenOCD to flash this device. After "
+                                       "reboot, this device will not be accessible to OpenOCD "
+                                       "anymore.", crp);
+                       if (!qn908x_info->allow_swd_disabled) {
+                               LOG_ERROR("Disabling SWD is not allowed, run "
+                                               "\"qn908x allow_brick\" before if you really want to "
+                                               "disable SWD. You won't be able to access this chip "
+                                               "anymore from OpenOCD.");
+                               return ERROR_FAIL;
+                       }
+               }
+       }
+
+       retval = qn908x_wait_for_idle(bank->target, QN908X_DEFAULT_TIMEOUT_MS);
+       if (retval != ERROR_OK)
+               return retval;
+
+       uint32_t smart_ctrl     = QN908X_FMC_SMART_CTRL_SMART_WRITEL_EN_MASK
+                                               | QN908X_FMC_SMART_CTRL_PRGML_EN_MASK
+                                               | QN908X_FMC_SMART_CTRL_MAX_WRITE(QN908X_FMC_SMART_CTRL_MAX_WRITE_RETRIES);
+       if (qn908x_info->num_blocks > 1) {
+               smart_ctrl      |= QN908X_FMC_SMART_CTRL_SMART_WRITEH_EN_MASK
+                                       | QN908X_FMC_SMART_CTRL_PRGMH_EN_MASK;
+       }
+       retval = target_write_u32(bank->target, QN908X_FMC_SMART_CTRL, smart_ctrl);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Write data page-wise, as suggested in the examples in section
+        * 28.5.2 "Flash write" of user manual UM11023 in revision 1.1
+        * (February 2018). */
+       while (count > 0) {
+               uint32_t next_offset = (offset & ~(QN908X_FLASH_PAGE_SIZE - 1)) + QN908X_FLASH_PAGE_SIZE;
+               uint32_t chunk_len = next_offset - offset;
+               if (chunk_len > count)
+                       chunk_len = count;
+
+               if (offset == 0
+                               && chunk_len >= QN908X_FLASH_IRQ_VECTOR_CHECKSUM_END
+                               && qn908x_info->calc_checksum) {
+                       /* write data prior to checksum */
+                       retval = target_write_buffer(bank->target, bank->base,
+                                       QN908X_FLASH_IRQ_VECTOR_CHECKSUM_POS, buffer);
+                       if (retval != ERROR_OK)
+                               return retval;
+
+                       /* write computed crc checksum instead of provided data */
+                       retval = target_write_u32(bank->target, bank->base + QN908X_FLASH_IRQ_VECTOR_CHECKSUM_POS, checksum);
+                       if (retval != ERROR_OK)
+                               return retval;
+
+                       retval = target_write_buffer(bank->target,
+                                       bank->base + QN908X_FLASH_IRQ_VECTOR_CHECKSUM_END,
+                                       chunk_len - QN908X_FLASH_IRQ_VECTOR_CHECKSUM_END,
+                                       buffer + QN908X_FLASH_IRQ_VECTOR_CHECKSUM_END);
+               } else {
+                       retval = target_write_buffer(bank->target, bank->base + offset,
+                                       chunk_len, buffer);
+               }
+
+               if (retval != ERROR_OK)
+                       return retval;
+
+               keep_alive();
+               buffer += chunk_len;
+               count -= chunk_len;
+               offset = next_offset;
+
+               /* Wait for FMC to complete write */
+               retval = qn908x_wait_for_idle(bank->target, QN908X_DEFAULT_TIMEOUT_MS);
+               if (retval != ERROR_OK)
+                       return retval;
+
+               /* Check if FMC reported any errors */
+               retval = qn908x_status_check(bank->target);
+               if (retval != ERROR_OK)
+                       return retval;
+       }
+
+       return retval;
+}
+
+static int is_flash_protected(struct flash_bank *bank, bool *is_protected)
+{
+       int retval;
+       uint32_t lock_stat;
+       retval = target_read_u32(bank->target, QN908X_FMC_LOCK_STAT_8, &lock_stat);
+       if (retval)
+               return retval;
+
+       *is_protected = false;
+       if (lock_stat & QN908X_FMC_LOCK_STAT_8_PROTECT_ANY)
+               *is_protected = true;
+
+       return ERROR_OK;
+}
+
+static int qn908x_probe(struct flash_bank *bank)
+{
+       int retval;
+       struct qn908x_flash_bank *qn908x_info = bank->driver_priv;
+       uint8_t info_page[QN908X_INFO_PAGE_CRC_END - QN908X_INFO_PAGE_CRC_START];
+       qn908x_info->num_blocks = 0;
+
+       /* When the SWD access to the RAM is locked by the LOCK_STAT_8 register we
+        * can't access the info page to verify the chip/bank version and it will
+        * read all zeros. This situation prevents the bank from being initialized
+        * at all so no other operation can be performed. The only option to
+        * re-flash the chip is to perform a mass_erase from SWD, which can be
+        * performed even if the mass_erase operation is locked as well.
+        * We attempt to read the info page and redirect the user to perform a
+        * mass_erase if we detect this situation. */
+       retval = target_read_memory(bank->target, QN908X_INFO_PAGE_CRC_START,
+                       sizeof(uint32_t), sizeof(info_page) / sizeof(uint32_t),
+                       info_page);
+       if (retval != ERROR_OK)
+               return retval;
+
+       const uint32_t crc_seed = 0xffffffff;
+       /* The QN908x uses the standard little endian CRC32 polynomial and all ones
+        * as seed. The CRC32 is however finalized by one last xor operation that
+        * is not part of the common CRC32 implementation, so we do that by hand */
+       uint32_t computed_crc = crc32_le(CRC32_POLY_LE, crc_seed,
+                       info_page, sizeof(info_page));
+       computed_crc ^= crc_seed;
+       uint32_t read_crc;
+       retval = target_read_u32(bank->target, QN908X_INFO_PAGE_CRC32, &read_crc);
+       if (retval != ERROR_OK)
+               return retval;
+
+       if (computed_crc != read_crc) {
+               uint32_t info_page_or = 0;
+               for (unsigned int i = 0; i < sizeof(info_page); i++)
+                       info_page_or |= info_page[i];
+               bool is_protected;
+               retval = is_flash_protected(bank, &is_protected);
+               if (retval != ERROR_OK)
+                       return retval;
+
+               if (info_page_or == 0 && is_protected) {
+                       LOG_ERROR("The flash or memory in this chip is protected and "
+                               "cannot be accessed from the SWD interface. However, a "
+                               "\"qn908x mass_erase\" can erase the device and lift this "
+                               "protection.");
+                       return ERROR_FAIL;
+               }
+
+               LOG_ERROR("Flash information page CRC32 mismatch, found 0x%08"
+                       PRIx32 " but computed 0x%08" PRIx32 ". Flash size unknown",
+                       read_crc, computed_crc);
+               return ERROR_FAIL;
+       }
+
+       uint32_t flash_size_fld = target_buffer_get_u32(bank->target,
+                       info_page + (QN908X_INFO_PAGE_FLASH_SIZE - QN908X_INFO_PAGE_CRC_START));
+
+       switch (flash_size_fld) {
+       case QN908X_FLASH_SIZE_512K:
+               qn908x_info->num_blocks = 2;
+               break;
+       case QN908X_FLASH_SIZE_256K:
+               qn908x_info->num_blocks = 1;
+               break;
+       default:
+               LOG_ERROR("Unknown Flash size field: 0x%08" PRIx32,
+                       flash_size_fld);
+               return ERROR_FAIL;
+       }
+
+       bank->size = qn908x_info->num_blocks * QN908X_FLASH_BLOCK_SIZE;
+       bank->write_start_alignment = 4;
+       bank->write_end_alignment = 4;
+
+       /* The flash supports erasing and protecting individual pages. */
+       bank->num_sectors = qn908x_info->num_blocks *
+               QN908X_FLASH_PAGES_PER_BLOCK;
+       bank->sectors = alloc_block_array(0, QN908X_FLASH_PAGE_SIZE,
+                       bank->num_sectors);
+       if (!bank->sectors)
+               return ERROR_FAIL;
+
+       retval = qn908x_init_flash(bank->target);
+       if (retval != ERROR_OK)
+               return retval;
+
+       LOG_INFO("Detected flash size: %d KiB", bank->size / 1024);
+
+       return ERROR_OK;
+}
+
+static int qn908x_auto_probe(struct flash_bank *bank)
+{
+       struct qn908x_flash_bank *qn908x_info = bank->driver_priv;
+       if (qn908x_info->num_blocks != 0)
+               return ERROR_OK;
+       LOG_DEBUG("auto_probe");
+       return qn908x_probe(bank);
+}
+
+static int qn908x_protect_check(struct flash_bank *bank)
+{
+       struct qn908x_flash_bank *qn908x_info = bank->driver_priv;
+
+       int retval = qn908x_read_page_lock(bank);
+       if (retval != ERROR_OK)
+               return retval;
+
+       for (uint32_t i = 0;
+                       i < qn908x_info->num_blocks * QN908X_FLASH_PAGES_PER_BLOCK;
+                       i++) {
+               /* A bit 0 in page_lock means page is locked. */
+               bank->sectors[i].is_protected =
+                       ((qn908x_info->page_lock.bits[i / 8] >> (i % 8)) & 1) ^ 1;
+       }
+       return ERROR_OK;
+}
+
+static int qn908x_get_info(struct flash_bank *bank,
+               struct command_invocation *cmd)
+{
+       uint32_t bootloader_version;
+       uint32_t chip_id;
+       uint8_t bluetooth[6];
+       int retval;
+       struct qn908x_flash_bank *qn908x_info = bank->driver_priv;
+
+       retval = target_read_u32(bank->target, QN908X_SYSCON_CHIP_ID, &chip_id);
+       if (retval != ERROR_OK) {
+               command_print_sameline(cmd, "Cannot read QN908x chip ID.");
+               return retval;
+       }
+       retval = target_read_u32(bank->target, QN908X_INFO_PAGE_BOOTLOADER_VER,
+                       &bootloader_version);
+       if (retval != ERROR_OK) {
+               command_print_sameline(cmd, "Cannot read from QN908x info page.");
+               return retval;
+       }
+
+       retval = target_read_memory(bank->target, QN908X_INFO_PAGE_BLUETOOTH_ADDR,
+                       1, sizeof(bluetooth), bluetooth);
+       if (retval != ERROR_OK) {
+               command_print_sameline(cmd, "Cannot read QN908x bluetooth L2 address.");
+               return retval;
+       }
+
+       command_print_sameline(cmd, "qn908x: chip id: 0x%" PRIx32, chip_id);
+
+       command_print_sameline(cmd, " bdaddr: "
+                       "%02" PRIx8 ":%02" PRIx8 ":%02" PRIx8
+                       ":%02" PRIx8 ":%02" PRIx8 ":%02" PRIx8,
+                       bluetooth[0], bluetooth[1], bluetooth[2],
+                       bluetooth[3], bluetooth[4], bluetooth[5]);
+
+       command_print_sameline(cmd, " bootloader: %08" PRIx32, bootloader_version);
+
+       command_print_sameline(cmd, " blocks: %" PRIu32, qn908x_info->num_blocks);
+
+       return ERROR_OK;
+}
+
+COMMAND_HANDLER(qn908x_handle_allow_brick_command)
+{
+       int retval;
+
+       struct target *target = get_current_target(CMD_CTX);
+       struct flash_bank *bank = NULL;
+
+       if (CMD_ARGC != 0)
+               return ERROR_COMMAND_SYNTAX_ERROR;
+
+       retval = get_flash_bank_by_addr(target, QN908X_FLASH_BASE, true, &bank);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* If get_flash_bank_by_addr() did not find the flash bank, it should have
+        * returned and error code instead of ERROR_OK */
+       assert(bank);
+       struct qn908x_flash_bank *qn908x_info = bank->driver_priv;
+
+       LOG_WARNING("Flashing images that disable SWD in qn908x is now allowed.");
+       qn908x_info->allow_swd_disabled = true;
+
+       return ERROR_OK;
+}
+
+COMMAND_HANDLER(qn908x_handle_disable_wdog_command)
+{
+       int retval;
+       struct target *target = get_current_target(CMD_CTX);
+
+       if (CMD_ARGC != 0)
+               return ERROR_COMMAND_SYNTAX_ERROR;
+
+       if (target->state != TARGET_HALTED) {
+               command_print(CMD, "Target not halted");
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       /* To change any value in the watchdog block (WDT) we need to first write
+        * 0x1ACCE551 to the LOCK register, and we can then set it back to any other
+        * value to prevent accidental changes to the watchdog. */
+       retval = target_write_u32(target, QN908X_WDT_LOCK, 0x1ACCE551);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = target_write_u32(target, QN908X_WDT_CTRL, 0);
+       if (retval != ERROR_OK)
+               return retval;
+
+       return target_write_u32(target, QN908X_WDT_LOCK, 0);
+}
+
+COMMAND_HANDLER(qn908x_handle_mass_erase_command)
+{
+       int retval;
+       bool keep_lock = false;
+       if (CMD_ARGC > 1)
+               return ERROR_COMMAND_SYNTAX_ERROR;
+       if (CMD_ARGC == 1) {
+               if (strcmp("keep_lock", CMD_ARGV[0]))
+                       return ERROR_COMMAND_ARGUMENT_INVALID;
+               keep_lock = true;
+       }
+
+       /* This operation can be performed without probing the bank since it is the
+        * only way to unlock a chip when the flash and ram have been locked. */
+       struct target *target = get_current_target(CMD_CTX);
+
+       retval = qn908x_setup_erase(target);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Check the mass-erase locking status for information purposes only. This
+        * lock applies to both the SWD and the code running in the core but can be
+        * bypassed in either case. */
+       uint32_t lock_stat_8;
+       retval = target_read_u32(target, QN908X_FMC_LOCK_STAT_8, &lock_stat_8);
+       LOG_DEBUG("LOCK_STAT_8 before erasing: 0x%" PRIx32, lock_stat_8);
+       if (retval != ERROR_OK)
+               return retval;
+       if ((lock_stat_8 & QN908X_FMC_LOCK_STAT_8_MASS_ERASE_LOCK_EN) == 0) {
+               LOG_INFO("mass_erase disabled by Flash lock and protection, forcing "
+                               "mass_erase.");
+       }
+       /* Set the DEBUG_PASSWORD so we can force the mass erase from the SWD. We do
+        * this regardless of the lock status. */
+       retval = target_write_u32(target, QN908X_FMC_DEBUG_PASSWORD, 0xCA1E093F);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Erase both halves of the flash at the same time. These are actually done
+        * sequentially but we need to send the command to erase both blocks since
+        * doing so in a locked flash will change the LOCK_STAT_8 register to 0x01,
+        * allowing us to access the (now erase) flash an memory. Erasing only one
+        * block at a time does not reset the LOCK_STAT_8 register and therefore
+        * will not grant access to program the chip. */
+       uint32_t erase_cmd = (1u << QN908X_FMC_ERASE_CTRL_HALF_ERASEH_EN_SHIFT) |
+               (1u << QN908X_FMC_ERASE_CTRL_HALF_ERASEL_EN_SHIFT);
+       LOG_DEBUG("Erasing both blocks with command 0x%" PRIx32, erase_cmd);
+
+       retval = target_write_u32(target, QN908X_FMC_ERASE_CTRL, erase_cmd);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = qn908x_wait_for_idle(target, QN908X_DEFAULT_TIMEOUT_MS);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = qn908x_status_check(target);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set the debug password back to 0 to avoid accidental mass_erase. */
+       retval = target_write_u32(target, QN908X_FMC_DEBUG_PASSWORD, 0);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* At this point the flash is erased and we are able to write to the flash
+        * since the LOCK_STAT_8 gets updated to 0x01 after the mass_erase. However,
+        * after a hard reboot this value will be realoaded from flash which after
+        * an erase is 0xff. This means that after flashing an image that doesn't
+        * set the protection bits we end up with a chip that we can't debug. We
+        * update this value to 0x01 unless "keep_lock" is passed to allow the SWD
+        * interface to debug the flash and RAM after a hard reset. */
+       if (keep_lock)
+               return retval;
+
+       retval = qn908x_init_flash(target);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Unlock access to RAM and FLASH in the last page of the flash and
+        * reloading */
+       retval = qn908x_wait_for_idle(target, QN908X_DEFAULT_TIMEOUT_MS);
+       if (retval != ERROR_OK)
+               return retval;
+
+       uint32_t smart_ctrl = QN908X_FMC_SMART_CTRL_SMART_WRITEH_EN_MASK |
+               QN908X_FMC_SMART_CTRL_PRGMH_EN_MASK;
+       retval = target_write_u32(target, QN908X_FMC_SMART_CTRL, smart_ctrl);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = target_write_u32(target, QN908X_FLASH_LOCK_ADDR,
+                       QN908X_FLASH_LOCK_ENABLE_MASS_ERASE);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = qn908x_wait_for_idle(target, QN908X_DEFAULT_TIMEOUT_MS);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Force a page_lock reload after the mass_erase . */
+       retval = qn908x_load_lock_stat(target);
+       if (retval != ERROR_OK)
+               return retval;
+
+       return retval;
+}
+
+static const struct command_registration qn908x_exec_command_handlers[] = {
+       {
+               .name           = "allow_brick",
+               .handler        = qn908x_handle_allow_brick_command,
+               .mode           = COMMAND_EXEC,
+               .help           = "Allow writing images that disable SWD access in their "
+                               "Code Read Protection (CRP) word. Warning: This can make your "
+                               "chip inaccessible from OpenOCD or any other SWD debugger.",
+               .usage          = "",
+       },
+       {
+               .name           = "disable_wdog",
+               .handler        = qn908x_handle_disable_wdog_command,
+               .mode           = COMMAND_EXEC,
+               .help           = "Disabled the watchdog (WDT).",
+               .usage          = "",
+       },
+       {
+               .name           = "mass_erase",
+               .handler        = qn908x_handle_mass_erase_command,
+               .mode           = COMMAND_EXEC,
+               .help           = "Erase the whole flash chip.",
+               .usage          = "[keep_lock]",
+       },
+       COMMAND_REGISTRATION_DONE
+};
+
+static const struct command_registration qn908x_command_handlers[] = {
+       {
+               .name           = "qn908x",
+               .mode           = COMMAND_ANY,
+               .help           = "qn908x flash controller commands",
+               .usage          = "",
+               .chain          = qn908x_exec_command_handlers,
+       },
+       COMMAND_REGISTRATION_DONE
+};
+
+const struct flash_driver qn908x_flash = {
+       .name                           = "qn908x",
+       .commands                       = qn908x_command_handlers,
+       .flash_bank_command     = qn908x_flash_bank_command,
+       .info                           = qn908x_get_info,
+       .erase                          = qn908x_erase,
+       .protect                        = qn908x_protect,
+       .write                          = qn908x_write,
+       .read                           = default_flash_read,
+       .probe                          = qn908x_probe,
+       .auto_probe                     = qn908x_auto_probe,
+       .erase_check            = default_flash_blank_check,
+       .protect_check          = qn908x_protect_check,
+       .free_driver_priv       = default_flash_free_driver_priv,
+};
diff --git a/tcl/target/qn908x.cfg b/tcl/target/qn908x.cfg
new file mode 100644 (file)
index 0000000..ac3e06b
--- /dev/null
@@ -0,0 +1,38 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# NXP QN908x Cortex-M4F with 128 KiB SRAM
+
+source [find target/swj-dp.tcl]
+
+set CHIPNAME qn908x
+set CHIPSERIES qn9080
+if { ![info exists WORKAREASIZE] } {
+  set WORKAREASIZE 0x20000
+}
+
+# SWD IDCODE (Cortex M4).
+set CPUTAPID 0x2ba01477
+
+swj_newdap $CHIPNAME cpu -irlen 4 -expected-id $CPUTAPID
+dap create $CHIPNAME.dap -chain-position $CHIPNAME.cpu
+
+set TARGETNAME $CHIPNAME.cpu
+target create $TARGETNAME cortex_m -dap $CHIPNAME.dap
+
+# SRAM is mapped at 0x04000000.
+$TARGETNAME configure -work-area-phys 0x04000000 -work-area-size $WORKAREASIZE
+
+# flash bank <name> qn908x <base> <size> 0 0 <target#> [calc_checksum]
+# The base must be set as 0x01000000, and the size parameter is unused.
+set FLASHNAME $CHIPNAME.flash
+flash bank $FLASHNAME qn908x 0x01000000 0 0 0 $TARGETNAME calc_checksum
+
+# We write directly to flash memory over this adapter interface. For debugging
+# this could in theory be faster (the Core clock on reset is normally at 32MHz),
+# but for flashing 1MHz is more reliable.
+adapter speed 1000
+
+# Delay on reset line.
+adapter srst delay 200
+
+cortex_m reset_config sysresetreq

Linking to existing account procedure

If you already have an account and want to add another login method you MUST first sign in with your existing account and then change URL to read https://review.openocd.org/login/?link to get to this page again but this time it'll work for linking. Thank you.

SSH host keys fingerprints

1024 SHA256:YKx8b7u5ZWdcbp7/4AeXNaqElP49m6QrwfXaqQGJAOk gerrit-code-review@openocd.zylin.com (DSA)
384 SHA256:jHIbSQa4REvwCFG4cq5LBlBLxmxSqelQPem/EXIrxjk gerrit-code-review@openocd.org (ECDSA)
521 SHA256:UAOPYkU9Fjtcao0Ul/Rrlnj/OsQvt+pgdYSZ4jOYdgs gerrit-code-review@openocd.org (ECDSA)
256 SHA256:A13M5QlnozFOvTllybRZH6vm7iSt0XLxbA48yfc2yfY gerrit-code-review@openocd.org (ECDSA)
256 SHA256:spYMBqEYoAOtK7yZBrcwE8ZpYt6b68Cfh9yEVetvbXg gerrit-code-review@openocd.org (ED25519)
+--[ED25519 256]--+
|=..              |
|+o..   .         |
|*.o   . .        |
|+B . . .         |
|Bo. = o S        |
|Oo.+ + =         |
|oB=.* = . o      |
| =+=.+   + E     |
|. .=o   . o      |
+----[SHA256]-----+
2048 SHA256:0Onrb7/PHjpo6iVZ7xQX2riKN83FJ3KGU0TvI0TaFG4 gerrit-code-review@openocd.zylin.com (RSA)