Mar 5, 2016

QPN DPP FW on Dialog DA14580

I ported the QPN-QK (QPC-nano: a bare metal, hard real-time C framework) for the Dialog's BLE chip DA14580 basic dev kit.  DA14580 has 84 KB ROM, but most of that is taken up by the bootloader and the Dialog's SDK (including the BLE functionality).  The OTP (on-time programmable) memory is only 32 KB, which is the code size limit of the Keil uVision free version.  The OTP is NOT executable; the intention is for the bootloader (in the ROM) to copy the code from OTP to (volatile) SRAM on boot or wakeup from deep sleep, during which time the volatile (vs. retained) SRAM content is lost.  The bootloader can also boot from UART, SPI flash, or I2C EEPROM, but in all cases, the code is first written to the SRAM before the real execution starts, so 32 KB of OTP is the practical code size limit.

DA1450x SDK (v 5.0.2) blinky example uVision 5 project

The SDK uses a boot_vectors.s (in peripheral_examples/shared/startup/) containing the vector table and Reset_Handler.

Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit


                LDR     R0, =SystemInit
                BLX     R0
                LDR     R0, =__main
                BX      R0
                ENDP


SystemInit() is a CPU/board specific function, supplied in system_ARMCM0.c (right next to the voot_vectors.s).  For the peripheral example, it uses the internal 16 MHz crystal oscillator.

void SystemInit (void)
{
  SetWord16(TIMER0_CTRL_REG,0x6);      // stop timer
  NVIC_DisableIRQ(SWTIM_IRQn);     // disable software timer interrupt
   
  if ((GetWord16(CLK_CTRL_REG) & RUNNING_AT_XTAL16M) == 0)
  {
    while( (GetWord16(SYS_STAT_REG) & XTAL16_SETTLED) == 0 );

    SetBits16(CLK_CTRL_REG , SYS_CLK_SEL ,0);
    while( (GetWord16(CLK_CTRL_REG) & RUNNING_AT_XTAL16M) == 0 ); // wait for actual switch
  }
}


__main() is not supplied in the SDK, but it's probably just a C runtime init and then a jump to the application main.

Memory map: use case 23

The boot_vectors.s also hardcodes the stack and the heap size:

Stack_Size      EQU     0x00000200

Heap_Size       EQU     0x00000100

The scatter file (equivalent to the GNU ld file) also hard codes the stack size.

LR_IROM1 0x20000000 0x00009800  {
...
    RW_IRAM1 (0x20009800 - 0x200) UNINIT 0x200 {         ; Stack
        .ANY (STACK)
    }

}

The system memory starting and ending address is for the memory map case 23, among the memory map choices listed in DA1258x SDK reference manual (Dialog document UM-B-051).  The blinky example puts everything (code, data, bss) in the 38 KB non-retention RAMP.

Debugger init

When debugging, the debugger ini file should match this starting address, as in this example:

RESET
E long 0x50000012 = 0xa4
E long 0x50003308 = 0x2e
LOAD %L
SP = _RDWORD (0x20000000)
$ = _RDWORD (0x20000004)


Let's briefly examine the system register settings 0x00A4 and 0x002E above.  SYS_CTRL_REG is configured for:
  • DEBUGGER_ENABLE
  • PAD_LATCH_EN
  • RET_SYSRAM: in development mode, system is NOT actually powered off (so that SysRAM is retained), and .data is NOT copied to SysRAM when the uC wakes up.
GP_CONTROL_REG = 0x2E = 0x17 << 1 + 1 specifies:
  • EM_MAP = 0x17 = case 23
  • BLE_WAKEUP_REQ: BLE wakes up

System init

To understand the system_init() called by main()--what kind of initialization does the example do?--, I need the Registers section of the DA14580 datasheet.

void system_init(void)
{
    SetWord16(CLK_AMBA_REG, 0x00);                 // set clocks (hclk and pclk ) 16MHz
    SetWord16(SET_FREEZE_REG,FRZ_WDOG);            // stop watch dog   
    SetBits16(SYS_CTRL_REG,PAD_LATCH_EN,1);        // open pads
    SetBits16(SYS_CTRL_REG,DEBUGGER_ENABLE,1);     // open debugger
    SetBits16(PMU_CTRL_REG, PERIPH_SLEEP,0);

}

Annotation of the above code:
  • APB (peripheral bus) and AHB (high speed bus) runs at the same frequency as the source (16 MHz above).
  • Freeze the watchdog HW.
  • Latch is transparent, pad can be retained (control signal is NOT retained)??
  • Retain SWD IO/CLK pins for debugging (instead of using them as GPIO).
  • Supply power to peripheral.  You can turn off the radio in the same PMU_CTRL_REG.
periph_init() repeats the above setup exactly anyway, so system_init() is technically unnecessary.  Once the entire peripheral block is powered up, the uC will get past this:

    while (!(GetWord16(SYS_STAT_REG) & PER_IS_UP));

Even though the peripheral block itself is now powered, clocks to individual HW (e.g. UART) must be turned on separately through the CLK_PER_REG.  The example blinks the GPIO LED, so that GPIO port is configured for output

    GPIO_ConfigurePin(LED_PORT, LED_PIN, OUTPUT, PID_GPIO, false);

Since the blinky example writes to the serial console, UART2 pins are configured.

    GPIO_ConfigurePin(UART2_GPIO_PORT, UART2_TX_PIN, OUTPUT, PID_UART2_TX, false);
    GPIO_ConfigurePin(UART2_GPIO_PORT, UART2_RX_PIN, INPUT, PID_UART2_RX, false);
    SetBits16(CLK_PER_REG, UART2_ENABLE, 1); // enable  clock for UART 2
    uart2_init(UART2_BAUDRATE, UART2_DATALENGTH);

Booting the FW from UART

During development, it is convenient to download and run (and debug) the FW from an IDE such as the Keil uVision.  But to use DA1458x as a slave uC in a production setting, a boot master needs to hold the FW and feed it to DA1458x during boot, according to the Dialog application note AN-B-001: DA1458x Booting from serial interface.  The boot code on the OTP checks all 1458x's serial peripherals sequentially, in the following steps:
DA1458x is acting as a slave device in steps 1 through 6, and as a master device for the rest of the steps.  Checking for an external master only happens once during reset, but the boot code loops through steps 7 and on 5 times before giving up and spinning, waiting for a SWD master intervention.  According to the above steps, the earliest step where an external UART master can boot the DA1458x is step 3, but for the fastest UART baud rate (8 bit, no parity in all cases) I must use step 4 (P0.2/3), as you can see in the following table:
The message protocol for transferring the FW image from the UART master to DA1458x is in the DA1458x connected to UART section of the same application note.  Dialog's own SmartSnippets GUI uses this protocol to download FW to DA1458x.

In a production device, where the boot device/pin is fixed, checking an external SPI master lengthens the boot latency.  Another Dialog document UM-B-012 DA14580/581/583 Creation of a secondary bootloader explains modifying the boot code to skip the unnecessary checks.

DPP (dining philosopher problem) solved with QPN on DA14580

The QK port for arm-cm discriminates itself for M0/M1 (which has no preemption feature) and M3/4/7.  I copied the example dpp_nucleo_l053r8 and modified only the bsp.c

In the CMSIS SystemInit(), I chose to use the internal 16 Mhz clock and power up the peripheral domain power.

void SystemInit (void)
{
    SetWord16(TIMER0_CTRL_REG,0x6);      // stop timer
    NVIC_DisableIRQ(SWTIM_IRQn);     // disable software timer interrupt
   
    if ((GetWord16(CLK_CTRL_REG) & RUNNING_AT_XTAL16M) == 0)
    {
        while( (GetWord16(SYS_STAT_REG) & XTAL16_SETTLED) == 0 );     // wait for XTAL16 settle
        SetBits16(CLK_CTRL_REG , SYS_CLK_SEL ,0);                     // switch to XTAL16
        while( (GetWord16(CLK_CTRL_REG) & RUNNING_AT_XTAL16M) == 0 ); // wait for actual switch
    }
    //SetWord16(GP_CONTROL_REG, 1 << 1 | 0);//Case 1 in DA1458x SDK ref Appendix A.1

    // system init
    SetWord16(CLK_AMBA_REG, 0x00);                 // set clocks (hclk and pclk ) 16MHz
    SetWord16(SET_FREEZE_REG,FRZ_WDOG);            // stop watch dog   
    SetBits16(SYS_CTRL_REG,PAD_LATCH_EN,1);        // open pads
    SetBits16(SYS_CTRL_REG,DEBUGGER_ENABLE,1);     // open debugger
    //SetBits16(PMU_CTRL_REG, PERIPH_SLEEP,0);     // exit peripheral power down

    // Power up peripherals' power domain
    //Q: Why set the PMU_CTRL_REG again with same value?
    SetBits16(PMU_CTRL_REG, PERIPH_SLEEP, 0);
}


The DA14580devkB board has only 1 user LED, so I used it for the philosopher[0] stat.

void BSP_displayPhilStat(uint8_t n, char const *stat) {
    if (stat[0] == 'h') {
        GPIO_SetActive( LED_PORT, LED_PIN);/* turn LED on  */
    }
    else {
        GPIO_SetInactive(LED_PORT, LED_PIN);/* turn LED off */
    }
}


The DPP example uses systick interrupt, but a typical low power FW will use a tick-less timer instead, rendering the current systick related code unnecessary in QF_onStartup():

void QF_onStartup(void) {
    /* set up the SysTick timer to fire at BSP_TICKS_PER_SEC rate */
    SetBits32(&SysTick->CTRL, SysTick_CTRL_ENABLE_Msk, 0);          // disable systick
    SetBits32(&SysTick->LOAD, SysTick_LOAD_RELOAD_Msk, BSP_TICKS_PER_SEC-1); // set systick timeout based on 1MHz clock
    SetBits32(&SysTick->VAL,  SysTick_VAL_CURRENT_Msk, 0);          // clear the Current Value Register and the COUNTFLAG to 0
    SetBits32(&SysTick->CTRL, SysTick_CTRL_TICKINT_Msk, 1); // generate interrupt
    SetBits32(&SysTick->CTRL, SysTick_CTRL_CLKSOURCE_Msk, 0);// use a reference clock (1 MHz)
    SetBits32(&SysTick->CTRL, SysTick_CTRL_ENABLE_Msk, 1);          // enable systick

    /* set priorities of ALL ISRs used in the system, see NOTE00
    *
    * !!!!!!!!!!!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    * Assign a priority to EVERY ISR explicitly by calling NVIC_SetPriority().
    * DO NOT LEAVE THE ISR PRIORITIES AT THE DEFAULT VALUE!
    */
    NVIC_SetPriority(SysTick_IRQn,   SYSTICK_PRIO);
    /* ... */

    /* enable IRQs... */
}


The DPP project structure looks like this:
Besides the board specific code in bsp.c (discussed above),  DA14580devkB folder contains the board specific files.

The uVision project generates afx file (equivalent to ELF file from gnu toolchain), but to generate a binary file (for booting the target from another uC for example), I added a post-build step in the User tab of the project option:

fromelf --bin --output .\out\app_uart.bin .\out\dpp_qk.axf

Unlike the example blinky project, QPN projects do NOT use heap space, and I specified that as a #define into the assembler (which assembles the boot_vectors.s)
The app FW bin (app_uart.bin) produced with these changes is 6.5 KB (which should contain everything that will be in the RAM, except the .bss section).  When I download the binary from uVision to the target over USB J-Link connection (DA14580devkB feature), I see the philosopher hungry state go on and off on the green user LED--as expected.

Adding QS real-time software tracing to QPN

When Miro Samek created QPN from QPC, he left out QSpy tracing feature of the QPC to keep the memory footprint down.  The QSpy sources are nicely refactored out in QPC:
  • include/
    • qs.h
  • source
    • qs.c
    • qs_fp.c
    • qs_pkg.h
    • qs_rx.c: receive and parse command from the QSpy viewer.  Unnecessary for passive tracing.
    • qstamp.c: build date.
I had to define the QSignal and QEvtSize data type sizes in qs_port.h, because QPN hard codes those types and QS cannot figure it out on its own.

#define Q_SIGNAL_SIZE 1
#define QF_EVENT_SIZ_SIZE 1
#define QF_CRIT_ENTRY(dummy) QF_INT_DISABLE()
#define QF_CRIT_EXIT(dummy)  QF_INT_ENABLE()


Also, on Cortex M0, there is no preemption of the ISR, so it's OK to just lock out the whole interrupt.  To use Q_SPY, I have to define Q_SPY to the CPP.
Now the app FW bin size is 9.4 KB, and the total size on RAM < 12 KB.

Appendix: More memory for the application FW

Since I am not interested in using DA14580's BLE HW, I can grab all memory that was reserved for BLE in the blinky example.  Unfortunately, there is no memory map configuration WITHOUT any memory for the BLE.  The best I can do is use case 1.
The starting address is still 0x20000000, but the end address is now 0x2000C000.  Whether I actually retain the top 6 KB is up to me (it does NOT retain out of reset).  The linker script (scatter file) for this modified memory map reserves only 1 KB for the stack, and removes heap altogether.

LR_IROM1 0x20000000 0xC0000  {
    ER_IROM1 0x20000000 0xB000  {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
        .ANY (+RW)
    }
    ER_IROM2 +0  {
     .ANY (+ZI)
    }
    RW_IRAM1 (0x2000C000 - 0x1000) UNINIT 0x1000 { ; Stack
        .ANY (STACK)
    }
}

The uC's memory map is changed during runtime by setting GP_CONTROL_REG (0x50003308) to (1 << 1) + 0 = 2; the 0 implies BLE_WAKEUP_REQ is turned off, like this:

    SetWord16(GP_CONTROL_REG, 1 << 1 | 0);
This should be done in SystemInit (which is called even before the C runtime initializes), as soon as the oscillator stabilizes.

Hard faulting as soon as I start!