Author Topic: Keil uVision + LPC1114 Cortex-M0 assembly question: delay by counting down value  (Read 2637 times)

0 Members and 1 Guest are viewing this topic.

Offline liudrTopic starter

  • Contributor
  • Posts: 38
  • Country: us
I was pretty good with x86 assembly a quarter century ago and just started learning ARM due to a possible teaching opportunity. So I started auditing a class. Task is simple. Trying to do a delay by counting down value in register subroutine (not proper but should be working). Set LED pin high, call delay, set LED pin low, call delay, repeat. We have a blink pattern.

This is the delay subroutine:

Code: [Select]
AREA program, CODE, READONLY
 EXPORT delay
;Takes value from R1 to perform delay

delay
PUSH {R0-R7, LR}

delay1
SUBS R1, R1, #1
BNE delay1

POP {R0-R7, PC}

END

Main routine Level1 is an infinite loop to repeat the pattern.

Code: [Select]
AREA PROGRAM, CODE, READONLY
 INCLUDE LPC11xx.inc
 IMPORT delay
 EXPORT Level1



Level1
PUSH{R0-R7,LR}
; Set the initial output on PIO1_5, PIO1_6, PIO1_7 as high
; 24.9s for 50 periods with 0 EORS instructions
; 24.9s for 50 periods with 1 EORS instructions
; 16.0s for 50 periods with 2 EORS instructions
; 15.7s for 50 periods with 3 EORS instructions
; 16.4s for 50 periods with 4 EORS instructions
; 15.6s for 50 periods with 5 EORS instructions
; 24.8s for 50 periods with 6 EORS instructions
; 24.9s for 50 periods with 7 EORS instructions
; 24.9s for 50 periods with 8 EORS instructions
; 25.5s for 50 periods with 9 EORS instructions
; 13.7s for 50 periods with 10 EORS instructions
; 16.4s for 50 periods with 11 EORS
; 16.8s for 50 periods with 12 EORS
;EORS R2, R2; Uncommenting causing 1s period, commenting causing 2s period, if R1=1.5M.
;MOVS R2,#0;
    ; Load the address of GPIO1DATA(last unmasked address base+0x3FFC) into R0
    LDR R0, =(GPIO1DATA); GPIO1DATA Base + 0x3FFC, address 0x5001 3FFC, Table 174, page 192
; Load the value (1) at bit location 5, 6 and 7 to set PIO1_5, PIO1_6, PIO1_7 high
MOVS R7, #(0x7); Three pins were set to 1, not lined up with pins 5-7, but later will
MOVS R7, R7, LSL #5 ; Pins 5 thru 7 pins start with bit 5.

main_loop
      LDR R1, [R0]
      ORRS R1,R7;
      ; Store the value of R1 into GPIO1DATA
      STR R1, [R0];
      ;EORS R1, R1

;----delay-----
      LDR R1, =1500000; delay1 has 2 instructions and should take 2 cycles to run. Then with the assumption of 12MHz clock, 6M cycles should be 12M instructions, .i.e. 1 second, not 2 seconds.
      BL delay;
      ; Load the value (0) at bit location 5, 6 and 7 to set PIO1_5, PIO1_6, PIO1_7 low
      MVNS R7, R7;
      LDR R1,[R0]
      ANDS R1,R7
      ; Store the value of R1 into GPIO1DATA
      STR R1, [R0];

      LDR R1, =1500000; The whole cycle takes 4 seconds, 2 sec on and 2 sec off. 0.7s for 2 subs 1 bne and R1=1.2M
      BL delay;

      MVNS R7, R7;
      ADDS R2, #1
      B main_loop


      POP {R0-R7,PC}
      END

The offending line is EORS R2, R2. It is NOT in the delay subroutine so it is NOT called during the register countdown. My comments before the line explains what I saw, regarding the delay period. If I have 0 or 1 such line, or 6-8 lines of EORS R2, R2, delay is about 2Hz. If I have 2-5 or 11, 12 lines of EORS R2, R2, I got about 3Hz. I carefully timed the LEDs for 50 cycles for each test. I've observed this on someone else's laptop with my breadboard, built a second breadboard with everything new. Others confirmed that adding some unimportant instructions caused some unexplainable changes to delays but they were students so they didn't investigate. I'm trying to understand ARM as a new architecture and will hopefully be an instructor so the motive to understand unexplainable behavior is there.
 

Offline bingo600

  • Super Contributor
  • ***
  • Posts: 2315
  • Country: dk
Are you fighting/hitting the "pipeline" flush ?

 
 

Offline liudrTopic starter

  • Contributor
  • Posts: 38
  • Country: us
Please do tell! I am just beginning to learn ARM. I know there is a 3-stage pipe line that does pre-fetch, decode, execute. I thought that was only affected in the delay subroutine. When BNE is branching, it takes 3 cycles, when it's not, it's 1 cycle. So if I count down 1.5million integers, it almost exclusively branches, and flush the pipe line(?). But I don't know if/how flash loading affects timing. Even if the internal flash runs at CPU clock, I'm not sure if it can be read one 32-bit at a time or maybe 4K but the CPU is not supposed to have instruction cache.

Here is a simplified version:

Code: [Select]
ENTRY
;EORS instructions (0-12 of them) go here

main_loop ; does blink
; Pin to HIGH
BL delay; with 1.5million value in R1
; Pin to LOW
BL delay; with 1.5million value in R1
B main_loop

You can see that the added EORS instructions are not even part of any loop. main_loop runs once every 0.5 seconds (supposedly) and delay loop runs once every 4 CPU cycles (1 for SUBS and 3 for BNE I guess).
 

Offline bingo600

  • Super Contributor
  • ***
  • Posts: 2315
  • Country: dk
 
The following users thanked this post: liudr

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4660
  • Country: us
Is there initialization code somewhere that sets up the system clock and flash wait states and such?

IMO, the CPU pipeline is unlikely to be the issue, but many ARM microcontrollers have some sort of "flash accelerator" in front of their flash memory, and that can be poorly documented, to the extent that whether having some loop span some memory boundary will change the timing is hard to determine.  (Could be as simple as a 16bit instruction being on the "wrong side" of a 32bit flash fetch (but probably not))

What happens if you replace the problem EORS with another instruction of the same length?

Are you using real hardware, or some simulator?  I take it that there is "boiler plate" code that calls Level1?
(What if you implement your own "main" code?  You don't HAVE to mess with the complex stuff like the clock configuration; you can run with the default (slower) clock and memory settings.)
You might try looking at disassembled binary, to check for suspicious memory boundaries, or instructions that didn't assemble the way you thought they would...
Keil does have a simulator - perhaps running under that might be helpful (although I doubt whether it accurately simulates flash timing.)
 
The following users thanked this post: liudr

Offline liudrTopic starter

  • Contributor
  • Posts: 38
  • Country: us
Thanks @bingo600. I'll look into those links.
 

Offline liudrTopic starter

  • Contributor
  • Posts: 38
  • Country: us
Thanks @westfw. I recognize your handle from my Arduino forum days! Hope all is good!

I'm using actual hardware, NXP LP1114/102. Based on the "latest" spec sheet, it doesn't even have SRAM but seems that it has 4KB. I'm using internal RC oscillator, ST LINK V2 programmer, some LEDs, resistors, and a reset button, no crystals. I'm not super impressed with their spec sheets. Regarding initialization code, I have a C project that did all the initialization. I think it's automatically generated from Keil:

Code: [Select]
/******************************************************************************
 * @file:    system_LPC11xx.c
 * @purpose: CMSIS Cortex-M0 Device Peripheral Access Layer Source File
 *           for the NXP LPC11xx Device Series
 * @version: V1.0
 * @date:    26. Nov. 2008
 *----------------------------------------------------------------------------
 *
 * Copyright (C) 2008 ARM Limited. All rights reserved.
 *
 * ARM Limited (ARM) is supplying this software for use with Cortex-M3
 * processor based microcontrollers.  This file can be freely distributed
 * within development tools that are supporting such ARM based processors.
 *
 * THIS SOFTWARE IS PROVIDED "AS IS".  NO WARRANTIES, WHETHER EXPRESS, IMPLIED
 * OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE APPLY TO THIS SOFTWARE.
 * ARM SHALL NOT, IN ANY CIRCUMSTANCES, BE LIABLE FOR SPECIAL, INCIDENTAL, OR
 * CONSEQUENTIAL DAMAGES, FOR ANY REASON WHATSOEVER.
 *
 ******************************************************************************/
#include <stdint.h>
#include "LPC11xx.h"

/*
//-------- <<< Use Configuration Wizard in Context Menu >>> ------------------
*/

/*--------------------- Clock Configuration ----------------------------------
//
// <e> Clock Configuration
//   <h> System Controls and Status Register (SCS)
//     <o1.4>    OSCRANGE: Main Oscillator Range Select
//                     <0=>  1 MHz to 20 MHz
//                     <1=> 15 MHz to 24 MHz
//     <e1.5>       OSCEN: Main Oscillator Enable
//     </e>
//   </h>
//
//   <h> Clock Source Select Register (CLKSRCSEL)
//     <o2.0..1>   CLKSRC: PLL Clock Source Selection
//                     <0=> Internal RC oscillator
//                     <1=> Main oscillator
//                     <2=> RTC oscillator
//   </h>
//
//   <e3> PLL0 Configuration (Main PLL)
//     <h> PLL0 Configuration Register (PLL0CFG)
//                     <i> F_cco0 = (2 * M * F_in) / N
//                     <i> F_in must be in the range of 32 kHz to 50 MHz
//                     <i> F_cco0 must be in the range of 275 MHz to 550 MHz
//       <o4.0..14>  MSEL: PLL Multiplier Selection
//                     <6-32768><#-1>
//                     <i> M Value
//       <o4.16..23> NSEL: PLL Divider Selection
//                     <1-256><#-1>
//                     <i> N Value
//     </h>
//   </e>
//
//
//   <h> CPU Clock Configuration Register (CCLKCFG)
//     <o7.0..7>  CCLKSEL: Divide Value for CPU Clock from PLL0
//                     <0-255>
//                     <i> Divide is CCLKSEL + 1. Only 0 and odd values are valid.
//   </h>
//
//
// </e>
*/
#define CLOCK_SETUP           1

#define MAIN_PLL_SETUP        33
#define MAIN_CLKSRCSEL_Val    0x00000000 // use RC oscillator
#define MAIN_PLL_M_Val        0x00000003
#define MAIN_PLL_P_Val        0x00000001
#define SYS_AHB_DIV_Val       1 /* 1 through 255, typical is 1 or 2 or 4 */

/*
//-------- <<< end of configuration section >>> ------------------------------
*/

/*----------------------------------------------------------------------------
  DEFINES
 *----------------------------------------------------------------------------*/
   
/*----------------------------------------------------------------------------
  Define clocks
 *----------------------------------------------------------------------------*/
#define XTAL        (12000000UL)        /* Oscillator frequency               */
#define OSC_CLK     (      XTAL)        /* Main oscillator frequency          */
#define IRC_OSC     ( 4000000UL)        /* Internal RC oscillator frequency   */
#define WDT_OSC     (  250000UL)        /* WDT oscillator frequency           */

/*----------------------------------------------------------------------------
  Clock Variable definitions
 *----------------------------------------------------------------------------*/
uint32_t ClockSource = IRC_OSC;
uint32_t SystemFrequency = IRC_OSC; /*!< System Clock Frequency (Core Clock)  */
uint32_t SystemAHBFrequency = IRC_OSC;

/**
 * Misc. clock generation modules
 *
 * @param  none
 * @return none
 *
 * @brief  Setup the microcontroller system.
 *         Initialize the System and update the SystemFrequency variable.
 */
void Main_PLL_Setup ( void )
{
  uint32_t regVal;

  ClockSource = OSC_CLK;
  LPC_SYSCON->SYSPLLCLKSEL = MAIN_CLKSRCSEL_Val;   /* Select system OSC */
  LPC_SYSCON->SYSPLLCLKUEN = 0x01;                 /* Update clock source */
  LPC_SYSCON->SYSPLLCLKUEN = 0x00;                 /* toggle Update register once */
  LPC_SYSCON->SYSPLLCLKUEN = 0x01;
  while ( !(LPC_SYSCON->SYSPLLCLKUEN & 0x01) ); /* Wait until updated */

  regVal = LPC_SYSCON->SYSPLLCTRL;
  regVal &= ~0x1FF;
  LPC_SYSCON->SYSPLLCTRL = (regVal | (MAIN_PLL_P_Val<<5) | MAIN_PLL_M_Val);
 
  /* Enable main system PLL, main system PLL bit 7 in PDRUNCFG. */
  LPC_SYSCON->PDRUNCFG &= ~(0x1<<7);
  while ( !(LPC_SYSCON->SYSPLLSTAT & 0x01) ); /* Wait until it's locked */

  LPC_SYSCON->MAINCLKSEL = 0x03; /* Select PLL clock output */
  LPC_SYSCON->MAINCLKUEN = 0x01; /* Update MCLK clock source */
  LPC_SYSCON->MAINCLKUEN = 0x00; /* Toggle update register once */
  LPC_SYSCON->MAINCLKUEN = 0x01;
  while ( !(LPC_SYSCON->MAINCLKUEN & 0x01) ); /* Wait until updated */

  LPC_SYSCON->SYSAHBCLKDIV = SYS_AHB_DIV_Val; /* SYS AHB clock, typical is 1 or 2 or 4 */

#if MAIN_PLL_SETUP
  SystemFrequency = ClockSource * (MAIN_PLL_M_Val+1);
#else
  SystemFrequency = ClockSource;
#endif
  SystemAHBFrequency = (uint32_t)(SystemFrequency/SYS_AHB_DIV_Val);
  return;
}

/**
 * Initialize the system
 *
 * @param  none
 * @return none
 *
 * @brief  Setup the microcontroller system.
 *         Initialize the System and update the SystemFrequency variable.
 */
void SystemInit (void)
{
  uint32_t i;

#ifdef __DEBUG_RAM   
  LPC_SYSCON->SYSMEMREMAP = 0x1; /* remap to internal RAM */
#else
#ifdef __DEBUG_FLASH   
  LPC_SYSCON->SYSMEMREMAP = 0x2; /* remap to internal flash */
#endif
#endif

#if (CLOCK_SETUP)                       /* Clock Setup */
  /* bit 0 default is crystal bypass,
  bit1 0=0~20Mhz crystal input, 1=15~50Mhz crystal input. */
  LPC_SYSCON->SYSOSCCTRL = 0x00;

  /* main system OSC run is cleared, bit 5 in PDRUNCFG register */
  LPC_SYSCON->PDRUNCFG &= ~(0x1<<5);
  /* Wait 200us for OSC to be stablized, no status
  indication, dummy wait. */
  for ( i = 0; i < 0x100; i++ );

#if (MAIN_PLL_SETUP)
  Main_PLL_Setup(); 
#endif

#endif /* endif CLOCK_SETUP */

  /* System clock to the IOCON needs to be enabled or
  most of the I/O related peripherals won't work. */
  LPC_SYSCON->SYSAHBCLKCTRL |= (1<<16);
  return;
}

I'm looking into it to see what it did. I added CLKOUT stuff after this so I can confirm it was using LR + PLL to result in a 48MHz main clock. I'm still reading this and need to refer to about a dozen tables for register values for exact initial status the code sets the CPU to.

Your flash accelerator idea makes sense to me. This acceleration is somehow interrupted by how many extra lines of EORS instructions, say if it's maybe 16 32-bit words long or something. Let me compile and debug the project so I can find actual locations of the instructions for rel distance calculations. I'll also use say SUBS R2, R2 which should be 2 bytes as well to replicate what I found with EORS.

From what I recall, Keil seems to assemble different .s files with BL.W instruction, 32-bit, instead of 6-bit, even though the files are all short and a short jump 16-bit instruction should do (maybe they are just proactive in case many object files may be linked together).
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4660
  • Country: us
Quote
From what I recall, Keil seems to assemble different .s files with BL.W instruction, 32-bit, instead of 6-bit, even though the files are all short and a short jump 16-bit instruction should do
I don't think that the CM0 has a 16bit BL instruction...
(But this is the sort of thing I meant when suggesting that the disassembly might not match what you thought you were doing.)

ARM v6m Architecture Reference Manual, Section A6.7.13 BL
Quote
Note                                                                                                                                                                        
Before the introduction of Thumb-2 technology, J1 and J2 in encodings T1 and T2 were both 1, resulting in a smaller branch range. The instructions could be executed as two separate 16-bit instructions, with the first instruction instr1 setting LR to PC + SignExtend(instr1<10:0>:'000000000000', 32) and the second instruction completing the operation. It is no longer possible to split the BL instruction into two 16-bit instructions in ARMv6T2, ARMv6-M and ARMv7. 
 

Offline Tation

  • Frequent Contributor
  • **
  • Posts: 311
  • Country: pt
Can use the ALIGN directive to force placement of an instruction in an address multiple of some power of two.
 
The following users thanked this post: liudr

Offline liudrTopic starter

  • Contributor
  • Posts: 38
  • Country: us
OK, the initialization code I posted didn't touch the FLASHCFG register which determines the wait state. With 48MHz CPU clock, this table says 3 cycles.
I couldn't find information regarding flash accelerator. What the spec sheet elaborated about flash was programming the flash. So I assume at 48MHz, the boot code sets 3 cycles for FLASH access based on the table.
Then I did some extensive tests. I'll only mention those conclusive ones:

I suspect "flash accelerator" scenario as @westfw suggested.

Test 3 (2nd photo):
I padded my main code with extra SUBS instructions for the delay subroutine to appear at a 16-byte boundary: 760h.
I then started padding delay with EORS instructions before the SUBS and BNE loop, .i.e EORS is NOT part of the loop.
I tested with 0 - 22 EORS instructions and got this pattern:
Every 8 extra EORS instructions make a complete repetition of observations. 5 out of 8 cases (congruent) have 3Hz flashing (accelerated) and 3 out of 8 cases (congruent) have 2Hz flashing (not accelerated). I'm not sure why the 5 accelerated vs 3 non-accelerated.
This means flash acceleration is affected by where the counting code is and where the subroutine starts. I wish I could draw a more detailed conclusion such as maybe there's a 16-byte aligned 8-instruction flash acceleration buffer or something but I couldn't. It would require 7 accelerated vs 1 non-accelerated cases.

Test 4 (3rd photo):
I added ALIGN 16 to my subroutine and started adding SUBS instructions to my main code. I observed (debug and assembly messages) that the delay subroutine gets pushed to the next 16-byte and next. I added 3-20 SUBSs in main to see this. In all cases I observed 3Hz flashing so they are all accelerated.

Regarding the 16bit BL, I'm sorry I don't know. I know there's B{cond} that are 16-bits. I'll read more on the instruction set.
So final conclusion, align to 16-bit boundary will stabilize the loop behavior and benefit from an unknown accelerator  :phew: I appreciate all your help!

 

Offline liudrTopic starter

  • Contributor
  • Posts: 38
  • Country: us
FYI, I also tested ALIGN 8 and added padding instructions in the caller routine to push the subroutine address on a 8-byte boundary. Under this scenario, the loop is accelerated whenever the subroutine starts at a 16-byte boundary and not accelerated whenever the subroutine starts at an 8-byte boundary, so 50%-50% of accelerated vs. non-accelerated. So a smaller boundary than 16 will NOT make a stable counting delay.
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4660
  • Country: us
Interesting results - thanks for posting the followup!

I guess most of the flash accelerators eliminate some or all of  the "configured wait states" when they can, which makes sense, but can cause issues when you're trying to write deterministic code.  Some processors will allow you to disable the acceleration to get determinism, or you can put the code in RAM (which usually has zero wait states on CM0 chips.)

I find the CM0 relatively annoying to code in assembly language.  Having only the Thumb-1 instructions means that a bunch of the mnemonics have weird limitations, losing the orthogonality that I thought was supposed to be a feature of RISC (and was present in the original ARM32 instructions.)
 

Offline grantb5

  • Regular Contributor
  • *
  • Posts: 160
  • Country: ca
Having been in similar shoes before, you may want to look at various textbooks on the subject and if they provide additional teaching resources. I found these ARM books pretty good, but I don't see anything LPC specific.

https://nicerland.com/

 

Offline cv007

  • Super Contributor
  • ***
  • Posts: 1061
Quote
I find the CM0 relatively annoying to code in assembly language.
Its about the only instruction set in the cortex-m family which is humanly usable, and even then you have to get a lot of details correct- alignment, register restrictions, variations on instruction syntax including the assembler syntax of the toolchain in use. Now move to an M3,M4,M33, etc. and your asm job just went x4 harder, assuming you want to take advantage of the extra instructions and not treat them all as an M0. Eventually you start creating a bunch of macros to help, and then you start to realize what you really want is a higher level language, which already exists. Its also not very easy to switch from something like Keil to gcc when using asm, as gcc asm syntax docs are not great.

Unless you want delays down to the clock cycle, which is seldom the case, use the hardware for timing which does not depend on wait state. A simple example using systick, which every cortex-m has-
https://godbolt.org/z/WrGqsxaYG
and if one wants to use asm, it will be easier to use the systick hardware than to count instructions (and account for wait states, or move to ram, etc.). The only variable is cpu clock speed which can be passed in or looked up, or the RVR value can simply be hard coded for known cpu speed. The online compiler is a good way to learn to read asm- write in a high level, see what gets produced. For the above example I used asm labels so one can more easily follow the higher level code, even at -Os.

If the systick timer is used in the app, use that for timing needs in the app, and if the systick timer is not setup yet (such as early startup code) or not used, use it to do basic blocking delays as done in the example. I use systick for system time (when very low power not needed), and in places such as early startup code before systick gets setup I use it as in the example. Much easier than using inline asm for basic delays.
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4660
  • Country: us
Quote
[CM0 is] about the only instruction set in the cortex-m family which is humanly usable, and even then you have to get a lot of details correct- alignment, register restrictions, variations on instruction syntax including the assembler syntax of the toolchain in use. Now move to an M3,M4,M33, etc. and your asm job just went x4 harder, assuming you want to take advantage of the extra instructions and not treat them all as an M0.
I strongly disagree.  It's not the "extra instructions" that make v7m more pleasant to program in assembler, it's the 32bit encodings of the "normal" instructions.  Things like having immediate operands for operations other than add and subtract, or immediate offsets larger than 32 for indexed addressing.
I mean "RSB (immediate): ARMv6-M only supports a value of 0."??  Come on!

 
The following users thanked this post: abyrvalg

Offline cv007

  • Super Contributor
  • ***
  • Posts: 1061
In addition to to the 3x more instructions to sort through, you also have many more restrictions/variations to deal with. For example using RSB for that v7m you now need to know about the 'Flexible second operand' and its restrictions and variations. With the more limited v6m instruction set, you use what you have which may mean more instructions to do the same job, but those additional instructions will be ones that you are already familiar with and using regularly.

For example, the M0 asm user will already know how to load a constant and also knows how to subtract-
https://godbolt.org/z/xrGxq7zYM
The limited instruction set did nothing to hamper doing a 'reverse subtract', and using what you already know vs learning another new instruction is not a bad thing. The RSB usage for the M3 is not really easier in this example due to the restrictions of the second operand.

 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf

 

-->