For STM32 microcontrollers reset vector is implemented as an array of pointers to handler functions. This allows set the reset vector from C.
void (* const __isr_vector[])() __attribute__((section(".isr_vector"))) = {
_stack_start,
reset_handler,
nmi_handler,
hard_fault_handler,
memmanage_handler,
busfault_handler,
usagefault_handler,
...
};
Array type syntax may be a bit overwhelming, so let's inspect it closer.
__attribute__((section(".isr_vector")))
part of the declaration tells linker that __isr_vector
array should be placed into .isr_vector
section so that it could be put into the appropriate place in memory.
out_t (*name)(in1_t, in2_t, in3_t)
is a general syntax for pointers to function. Here name
is a name of the function pointer variable, out_t
is the function output type, in1_t
, in2_t
etc. are the types of the function arguments. The following snippet demonstrates usage of the function pointers:
#include <stdio.h>
int
sum(int a, float b)
{
return a + (int) b;
}
int
mult(int a, float b)
{
return a * (int) b;
}
int
main(void)
{
int (*fun_ptr)(int, float) = ∑
printf("%d\n", (*fun_ptr)(10, 2.2)); // -> 12
fun_ptr = &mult;
printf("%d\n", (*fun_ptr)(10, 2.2)); // -> 20
return 0;
}
Finally, const __isr_vector[]
indicates that variable is actually an immutable array of such pointers. We don't need to specify array size because it could be inferred from the code.
Array items should have the same type as defined in __isr_vector
array. We may define them in the following manner.
extern void _stack_start();
__attribute__((weak)) void
reset_handler()
{
for (;;) {}
}
__attribute__((weak)) void
default_handler()
{
for (;;) {}
}
__attribute__((weak)) void
hard_fault_handler()
{
for (;;) {}
}
_stack_start
is actually not a function pointer but a stack start address symbol defined in linker script. We cast it as a function pointer as a workaround.
Shown above handler functions are trivial infinite loops and act as placeholders. They are marked __attribute__((weak))
to allow user redefine them without removing these definitions.
Other functions in the reset vector are defined as weak aliases to avoid code duplication and save memory space. They may be overridden by user to implement a specific handler.
void nmi_handler() __attribute__((weak, alias("hard_fault_handler")));
void memmanage_handler() __attribute__((weak, alias("hard_fault_handler")));
void svc_handler() __attribute__((weak, alias("default_handler")));
void systick_handler() __attribute__((weak, alias("default_handler")));
Defining fault handlers as infinite loops may be helpful during the debugging. When failure occurs value of
PC
register will point to the handler of the specific event (e.g. hard fault, memory fault). Other then pushingR7
to stack, these handlers do not modify registers, so their values may be examined.
As for assembly programs, .data
region needs to be relocated to the RAM during the initialization process. Linker script defines symbols with LMA and VMA addresses of the section but their use in C code is not as straightforward as in assembly.
_data_lma = LOADADDR(.data);
SECTIONS {
...
.data : {
_data_vma = .;
*(.data)
_data_evma = .;
} > RAM AT>FLASH
Let's compare definition of the linker defined symbol _data_vma
with regular variable foo
.
Symbol table Memory
┌──────────┐ ┌──────┐
│foo ├──────►│42 │
├──────────┤ ├──────┤
│_data_vma ├──────►│??? │
└──────────┘ └──────┘
When variable is defined in C
int foo = 42;
symbol foo
is placed into the symbol table with the address of the value. Space for the variable is allocated in memory. Writing to the foo
requires first retrieving its address from the symbol table with the subsequent write to that memory address. On the other hand, symbols defined by linker appear in symbol table just like regular variables but do not have anything allocated in the memory. Value of the _data_vma
in C will be meaningless. To retrieve address from the linker script defined symbol we actually need to take address of _data_vma
.
extern unsigned long _data_vma;
void
reset_handler()
{
unsigned long *data_vma_address = &_data_vma;
}
Thus, to relocate .data
section the following code could be used.
extern unsigned long _data_vma, _data_evma, _data_lma;
void
reset_handler()
{
// relocate data section
memcpy(&_data_vma, &_data_lma, &_data_evma-&_data_vma);
}
Embedded C programs make extensive use of macro constants.
#define GPIO_C 0x40011000
#define GPIOx_CRL 0x00
#define GPIOx_CRH 0x04
#define GPIOx_IDR 0x08
#define GPIOx_ODR 0x0c
#define GPIOx_BSSR 0x10
#define GPIOx_BRR 0x14
#define GPIOx_LCKR 0x18
In contrast with constant variables, macro constants appear as if they were hard-coded. Therefore, they are not placed into the .rodata
section but into the assembly instructions themselves (where allowed by assembly). Unused macro constants are discarded and don't take place in memory.
In order the enable GPIOC port clock we used the following lines. Let's examine them more closely.
volatile unsigned long *const iopcen = (unsigned long *) (RCC_BASE + RCC_AHB2ENR);
*iopcen |= (1 << 4);
Compilers analyze and modify code in order to produce smaller, faster program executable. This includes reducing a number of memory load instructions by caching memory value in registers assuming that the value in memory does not change. This assumption is correct when accessing regular memory (except for the multi-threading applications) but not when specified location is a memory mapped hardware register. In this case keyword volatile
tells the compiler that variable value may change in memory without explicit write to that location.
Rule of thumb: always declare register variables as
volatile
*const
vs const*
Declarations
volatile unsigned long *const iopcen;
and
volatile unsigned long const *iopcen;
are very different.
C definitions read from right to left.
volatile unsigned long *const iopcen;
is a constant pointer to unsigned long. We can change the value at the address pointer is pointing to but we cannot change that address.volatile *unsigned long const iopcen;
is a constant unsigned long pointer (pointer to the constant unsigned long). We cannot change the value at the address pointer is pointing to but we can change that address.Most of the time it's desirable to modify just the requires bits in the hardware register preserving all other values. It's convenient to use bitmasks in this read-modify-write pattern.
Bitmask is a technique of modifying selected bits of the binary value by applying logical operations.
or
operator.0b1011_0001 |or
0b0000_1100
-----------
0b1011_1101
Second argument of or
operator is a bitmask. It has 1
at each position we wish to set to 1
in the original value.val = val | bitmask;
val |= bitmask;
and not
operators.0b1011_0001 |and not
0b1001_0000
-----------
0b1011_0001 |and
0b0110_1111
-----------
0b0010_0001
Second argument of and not
operator is a bitmask. It has 1
at each position we wish to set to 0
in the original value.val = val & ~bitmask;
val &= ~bitmask;
xor
operator.0b1011_0001 |xor
0b0110_0000
-----------
0b1101_0001
Second argument of xor
operator is a bitmask. It has 1
at each position we wish to boggle bit in the original value (set 0
to 1
and 1
to 0
).val = val ^ bitmask;
val ^= bitmask;
Left shift operator is a very readable way of creating bitmasks. Operator val<<n
may be read as "put val
at the n
th position".
0b0000_0101<<5
-----
0b1010_0000
.bss
section in RAM above stack.https://sourceware.org/binutils/docs/ld/Source-Code-Reference.html