📜 ⬆️ ⬇️

ARMs for the smallest: layout-2, interrupts and hello world!



I found the opportunity to "finish off" the cycle with another article, where I will summarize a little. In fact, it’s only now that we’ve gotten to the point where we usually start programming:


Previous articles of the cycle:

')
Code examples from the article: https://github.com/farcaller/arm-demos



Last time, we figured out which sections we could encounter in the linked application and what their typical contents were. In particular, we dealt with .data and .bss . Remember that .data stores global (static) variables with the value specified during compilation. This section must be copied from flash memory to operational. Global variables with zero value are stored in .bss , it must be zeroed out.

In typical conditions, the procedures from crt0.a (Wikipedia suggests that this name means C RunTime 0, where 0 means the very beginning of the application’s life). Today we will write an analogue crt0 for our toy platforms.

Disclaimer. In GNU ld, many of the same things can be done in different ways, using syntax variations and layout flags. All of the methods described below are a figment of my imagination, written under the influence of LPCXpresso layout scripts. If you know a more effective method of solving any of the described situation, write to me!

Initialization of data in memory


See the file 04-helloworld/platform/protoboard/layout.ld . In general, there are no significant changes relative to the previous version: several constants, a description of the memory section. Let's take a look at the .data section for an example:
 .data : ALIGN(4) { _data = .; *(SORT_BY_ALIGNMENT(.data*)) . = ALIGN(4); _edata = .; } > ram AT>rom = 0xff 


A .data section is written to the output file with alignment of 4 bytes (i.e., if the cursor points to 0x00000101 before this section, then .data will start from 0x00000104). The section is in RAM ( > ram ), but is loaded from flash memory ( AT>rom ).

The construction =0xff sets the fill pattern. If unaddressed bytes are formed in the output section, their value will be set to the value of the filler byte. 0xff is selected for the reason that the erased flash memory is all units, i.e., the 0xff record (as opposed to 0x00, for example) is an empty operation.

Further, the current cursor position is stored in _data . Since the section is in RAM, _data will indicate its very beginning, in this case: 0x10000000.

Alternately, all source sections with names beginning with .data from all input files are copied to the section, while sorting them by size. Sorting plays a very important role, consider it on an example:
 uint16_t static_int = 0xab; uint8_t static_int2 = 0xab; uint16_t static_int3 = 0xab; uint8_t static_int4 = 0xab; 


Four variables for the .data section are defined here. What gets into the final file?
 .data 0x0000000010000000 0xc load address 0x00000000000007b0 0x0000000010000000 _data = . *(.data*) .data.static_int2 0x0000000010000000 0x1 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000000 static_int2 *fill* 0x0000000010000001 0x3 ff .data.static_int3 0x0000000010000004 0x4 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000004 static_int3 .data.static_int4 0x0000000010000008 0x1 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000008 static_int4 *fill* 0x0000000010000009 0x1 ff .data.static_int 0x000000001000000a 0x2 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x000000001000000a static_int 0x000000001000000c . = ALIGN (0x4) 0x000000001000000c _edata = . 

Note the *fill* bytes that align the variables along the word boundary. Due to an unsuccessful order, we lost 4 bytes just like that. Repeat the operation, this time using SORT_BY_ALIGNMENT:
 .data 0x0000000010000000 0x8 load address 0x00000000000007b0 0x0000000010000000 _data = . *(SORT(.data*)) .data.static_int3 0x0000000010000000 0x4 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000000 static_int3 .data.static_int 0x0000000010000004 0x2 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000004 static_int .data.static_int2 0x0000000010000006 0x1 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000006 static_int2 .data.static_int4 0x0000000010000007 0x1 build/d0f0154f60ed1a9c2083183e7c731846451d2bdb_helloworld.o 0x0000000010000007 static_int4 0x0000000010000008 . = ALIGN (0x4) 0x0000000010000008 _edata = . 

Variables are neatly sorted, and we saved a bunch (33%) of memory!

Let us return to the cursor, which now points immediately to the end of all .data . Construction . = ALIGN(4) aligns the cursor (if there is not enough data in the input sections for full alignment) along the word boundary. The final value is recorded in _edata .

In addition to the addresses in memory, we need to know where the section is in flash memory. For this, the symbol is declared at the beginning of the script: _data_load = LOADADDR(.data) . LOADADDR is a function that returns the section load address. In addition to it there are some more interesting functions: ADDR returns the “virtual” address, SIZEOF - section size in bytes.

Take a look at the initialization code of the .data section, 04-hello-world/platform/common/platform.c :
 uint32_t *load_addr = &_data_load; for (uint32_t *mem_addr = &_data; mem_addr < &_edata;) { *mem_addr++ = *load_addr++; } 

In the loop, we copy the values ​​from load_addr to mem_addr .

Typically, this initialization is carried out as early as possible, if possible, as one of the very first tasks. There is a very reasonable explanation for this: prior to initialization, access to global variables from C will return garbage. In our case, the initialization is carried out after calling platform_init , since this function does not depend on the data in .data / .bss , and its execution will allow you to execute the subsequent code faster, which ultimately will give a performance boost. The downside was the emergence of a separate platform_init_post , where the global variable is initialized by the system bus frequency value.

The last section - /DISCARD/ - is special, it's a kind of / dev / null linker. All incoming sections will simply be thrown away (as you remember, if a section is not explicitly indicated, it will be automatically added to the appropriate memory area). This section is described more for clarity, as the input sections in the case of ARMv6-M0 are guaranteed to be empty.

About different interruptions


Pay your attention to the slightly modified first section .text , where two new ones fall: .isr_vector and .isr_vector_nvic . Both are wrapped in a KEEP instruction, which prevents the linker from “optimizing” them as superfluous. .isr_vector contains a common interrupt table for the Cortex-M, which can be platform/common/isr.c in the file platform/common/isr.c :

 __attribute__ ((weak)) void isr_nmi(); __attribute__ ((weak)) void isr_hardfault(); __attribute__ ((weak)) void isr_svcall(); __attribute__ ((weak)) void isr_pendsv(); __attribute__ ((weak)) void isr_systick(); __attribute__ ((section(".isr_vector"))) void (* const isr_vector_table[])(void) = { &_stack_base, main, // Reset isr_nmi, // NMI isr_hardfault, // Hard Fault 0, // CM3 Memory Management Fault 0, // CM3 Bus Fault 0, // CM3 Usage Fault &_boot_checksum, // NXP Checksum code 0, // Reserved 0, // Reserved 0, // Reserved isr_svcall, // SVCall 0, // Reserved for debug 0, // Reserved isr_pendsv, // PendSV isr_systick, // SysTick }; 


As you can see, we moved away from declaring a table in an assembly file and describe it in C terminology. Independent interrupt handlers were also introduced (instead of one common hang ). All of these default handlers perform an infinite loop (although I shoved the debug LED a couple of times in isr_hardfault I wrote examples for the article), but since they are declared with the weak attribute, they can be redefined in any other file. For example, timer.c has its own implementation of isr_systick , which will fall into the final image.

The continuation of the table is made in a similar structure isr_vector_table_nvic , since it already depends on the specific processor, but the essence remains the same.

And about interruptions


Let's say a little more about interrupts. The general essence of interrupts is a call to the handler as a reaction to any external events (relative to the code that is executed at the time of the event). A nice feature of Cortex-M: the processor will pack / unpack register values ​​itself, so that interrupts can be written as normal functions in C. Moreover, the nesting of interrupts will also be worked out automatically.

NVIC - nested vector interrupt controller processes interrupts from peripherals behind the ARM core. It allows you to set different priorities for different interrupts, disable them centrally or generate an interrupt programmatically.

Let's look at a new systick-based timer implementation:
 static volatile uint32_t systick_10ms_ticks = 0; void platform_delay(uint32_t msec) { uint32_t tenms = msec / 10; uint32_t dest_time = systick_10ms_ticks + tenms; while(systick_10ms_ticks < dest_time) { __WFI(); } } // override isr_systick from isr.c void isr_systick(void) { ++systick_10ms_ticks; } 

The standby cycle puts the processor in the interrupt standby mode (sleep mode) until the system counter exceeds the required value. At the same time, every 10 ms, SysTick overflows and generates an interrupt, by which the isr_systick increments the counter by 1. Note that systick_10ms_ticks declared as volatile , this makes the compiler realize that the value of this variable can (and will) change outside the current context, and it should be re-read each time from RAM (where the interrupt handler will change it).

libgcc


In this code, we first use the division operation. It would seem that there is a difficult, but in Cortex-M0 there is no hardware instruction for the division :-). The compiler is aware of this, and instead of a division instruction, it inserts a call to the function __aeabi_uidiv , which divides numbers programmatically. This function (and several similar ones) are implemented in the compiler support library: libgcc.a. Unfortunately, our linker knows nothing about it, and we stumble upon an unpleasant error:
 build/5a3e7023bbfde5552a4ea7cc57c4520e0e458a53_timer.o: In function `platform_delay': timer.c:(.text.platform_delay+0x4): undefined reference to `__aeabi_uidiv' 

The correct solution is to replace the linker call directly with the gcc call, which will already figure out where to link. True, gcc can be a little overdoing, so we tell it via -nostartfiles that we have our own initialization code, and through -ffreestanding , that our application is independent and does not depend on any OS.

Finally, hello habr!


This version is somewhat significant, as it has a UART driver, which means that we will see the real operation of our code not only by the flashing LED. But first, the driver:
platform/protoboard/uart.c
 extern uint32_t platform_clock; void platform_uart_setup(uint32_t baud_rate) { NVIC_DisableIRQ(UART_IRQn); 
First of all, we turn off the interrupt on NVIC in case it was turned on.
  LPC_SYSCON->SYSAHBCLKCTRL |= (1<<16); LPC_IOCON->PIO1_6 &= ~0x07; LPC_IOCON->PIO1_6 |= 0x01; LPC_IOCON->PIO1_7 &= ~0x07; LPC_IOCON->PIO1_7 |= 0x01; 
Next, we will turn on the microcontroller unit, which is responsible for pin setting, and configure them in TXD / RXD UART mode. This code spilled a lot of my blood when I tried to understand why the UART after a reboot does not work. Be careful, sometimes the obvious things are turned off by default!
  LPC_SYSCON->SYSAHBCLKCTRL |= (1<<12); LPC_SYSCON->UARTCLKDIV = 0x1; 
Now you can turn on the UART itself, and at the same time set the input frequency divider.
  LPC_UART->LCR = 0x83; uint32_t Fdiv = platform_clock //   / LPC_SYSCON->SYSAHBCLKDIV //      / LPC_SYSCON->UARTCLKDIV //    UART / 16 //   16,   / baud_rate; // , ,   LPC_UART->DLM = Fdiv / 256; LPC_UART->DLL = Fdiv % 256; LPC_UART->FDR = 0x00 | (1 << 4) | 0; LPC_UART->LCR = 0x03; 
In addition to the classic 8N1 mode, we open access to the output dividers, which set the bitrate. Calculate dividers and write them into registers. For the curious, the formula is in section 13.5.15 of the manual. In addition, it describes an additional divider for even more accurate bodrate. In my tests, the 9580 worked quite well :-)
  LPC_UART->FCR = 0x07; volatile uint32_t unused = LPC_UART->LSR; while(( LPC_UART->LSR & (0x20|0x40)) != (0x20|0x40) ) ; while( LPC_UART->LSR & 0x01 ) { unused = LPC_UART->RBR; } 
We turn on the FIFO, reset it, make sure that there are no strange data in the registers.
  // NVIC_EnableIRQ(UART_IRQn); // LPC_UART->IER = 0b101; 
Enable interrupts to receive (in fact, no). There is no interrupt handler in the example, so there is no need for an interrupt.

For LPC1768, the code is very similar, so I will not analyze it. I note only that there all the peripherals are included at boot time, which simplifies the situation.

An important point: mbed has three UARTs brought outside, and several pin options for each. Since USB communication would take significantly more code, you will have to hook the FTDI-cord on the UART, in the example - these are the P13 / P14 pins.

Summing up


We have dealt with the linker, we have a ready core on which to expand the base and write drivers. Or even take CMSIS and the demo from the manufacturer (just read the code, the examples in LPCXpresso have typos of different degrees of sadness).

I have enough ideas for further articles, but there was not enough time, too many interesting things are not yet programmed! I will try, nevertheless, to return to the “microcosm” of embeddings after the “macroworld” of office days.

PS As always, thank you very much pfactum for reading the text.

Creative Commons License This work is available under the Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported license . The program text of the examples is available under the Unlicense license (unless otherwise indicated in the headers of the files). This work is written solely for educational purposes and is in no way affiliated with the current or previous employers of the author.

Source: https://habr.com/ru/post/194816/


All Articles