Is a company from Norway that produces semiconductors.
Uses ARM-Cortex M0 as their core mcu.
Uses ARM-Cortex M4 as their core mcu.
Uses ARM-Cortex M33 as their core mcu.
LTE
The system-on-chip (SoC) that is on the microbit device is a nRF52833, which contains the 64MHz Arm Cortex-M4.
nRF52833 Product Specification
I've also got a nRF52-DK which has a N52832 SoC:
nRF52832 Product Specification
The actual number can be found on the chip:
This is called Two-Wire Inteface in nRF so the register names will be TWI1 etc.
TODO:
If an application does not use a SoftDevice or a Master Boot Record then the Flash memory should be 0x0.
How do I know if the application uses a SoftDevice?
For a bare-metal assembly example where I'm not using anything except a linker
script that I've written if there anything reason why I should not use 0x0 as
the origin of the Flash memory?
No, I don't think so.
When using a SoftDevice the linker would need to have access to the SoftDevice object file during the linking, and the linker script would have to have memory configuration and section for it.
A SoftDevice is a wireless protocol stack that complements an nRF5 Series System on Chip (SoC).
SoftDevices are a closed source C binary written by Nordic for their microcontrollers that sits at the bottom of flash and is called first on startup. The softdevice then calls your application or bootloader or whatever is sitting directly after it in flash.
So this will affect the linker-script and the origin of flash memory.
- s112 BLE protocol stack. Peripheral only
- s113 BLE protocol stack. Peripheral only
- s122 BLE protocol stack. Central only
- s132 BLE protocol stack. Peripheral and Central
Each peripheral is assigned a block of 0x1000 (4096) bytes of address space. So that gives 4096 x 8 = 32768, providing 1024, 32 bit registers.
0x40000000 ID = 0 1073741824
0x40001000 ID = 1 1073741824 + 4096 = 1073745920 = 0x40001000
0x40002000 ID = 2 1073741824 + 2*4096 = 1073750016 = 0x40002000
...
0x4001F000 ID = 31 1073741824 + 31*4096 = 1073868800 = 0x40001F00
Most peripherals have an enable register that is used to enable the peripheral in question. For example, lets take GPIO P0:
ID Base Address
0 0x50000000
The pins available for each port are PIN0 to PIN31 and each pin can be configured using the PIN_CNF[n].
Peripherals are controlled by the CPU writing config registers and task registers. When a peripheral wants to signal that something has occurred it will write to the event register.
An example of a service implementation can be found in the nrf-sdk, for example
the LED Button Service (LBS) can be found in
/components/ble/ble_services/ble_lbs
In nrf/ble/main.c we create use a macro:
BLE_LBS_DEF(m_lbs);
There is a make target named 'pre' in the nrf/ble directory which will output the code generated by the pre-processor:
static ble_lbs_t m_lbs;
static nrf_sdh_ble_evt_observer_t m_lbs_obs __attribute__ ((section("." "sdh_ble_observers2"))) __attribute__((used)) = {
.handler = ble_lbs_on_ble_evt,
.p_context = &m_lbs
};
If we start we can see that there is first a struct named ble_lbs_t
which
is a struct for the LED Button Service:
struct ble_lbs_s {
uint16_t service_handle; /**< Handle of LED Button Service (as provided by the BLE stack). */
ble_gatts_char_handles_t led_char_handles; /**< Handles related to the LED Characteristic. */
ble_gatts_char_handles_t button_char_handles; /**< Handles related to the Button Characteristic. */
uint8_t uuid_type; /**< UUID type for the LED Button Service. */
ble_lbs_led_write_handler_t led_write_handler; /**< Event handler to be called when the LED Characteristic is written. */
};
And we also define a observer for the service:
typedef struct {
nrf_sdh_ble_evt_handler_t handler; //!< BLE event handler.
void * p_context; //!< A parameter to the event handler.
} const nrf_sdh_ble_evt_observer_t;
typedef void (*nrf_sdh_ble_evt_handler_t)(ble_evt_t const * p_ble_evt, void * p_context);
Notice that there is a section specifed for this static variable which is
sdh_ble_observers2
and this will be matched in the linker script.
In main.c we have the following call:
err_code = sd_ble_gap_device_name_set(&sec_mode,
(const uint8_t *)DEVICE_NAME,
This function is defined in ble_gap.h as:
SVCALL(SD_BLE_GAP_DEVICE_NAME_SET,
uint32_t,
sd_ble_gap_device_name_set(ble_gap_conn_sec_mode_t const *p_write_perm, uint8_t const *p_dev_name, uint16_t len));
Which will be expanded by the pre-processor into:
__attribute__((naked)) __attribute__((unused))
static uint32_t sd_ble_gap_device_name_set(
ble_gap_conn_sec_mode_t const *p_write_perm,
uint8_t const *p_dev_name,
uint16_t len) {
__asm( "svc %0\n" "bx r14" : : "I" ( SD_BLE_GAP_DEVICE_NAME_SET) : "r0" );
}
The _naked
attribute means that this function is an embedded assembly function
which is a function which can be written entierly using __asm
statements. The
compiler will not generate a prologue or epilogue for these type of functions.
The unused
attribute just prevents the compiler from generating a warning if
this function is not referenced anywhere.
Taking a closer look at the inline assembly code we see the following code:
__asm("svc %0\n"
"bx r14"
: // output operands
: "I" (SD_BLE_GAP_DEVICE_NAME_SET) // input operands
: "r0" ); // clobbered registers
The input operand is given the contraint I
which specifes that an immediate
integer or string literal operand which in this case is
SD_BLE_GAP_DEVICE_NAME_SET
and this passed as the argument to the svc
arm
instruction. This is the supervisor instruction call. So there will be a entry
in the vector table for SVCALL
, entry 11. This handler will use the number
to take some action. We can see the number below which is defined in
headers/ble_gap.h
:
enum BLE_GAP_SVCS
{
...
SD_BLE_GAP_DEVICE_NAME_SET = BLE_GAP_SVC_BASE + 16, /**< Set Device Name. */
...
The supervisor call (SVC) instruction triggers an exception and takes a number which the exception handler can extract and take different actions on depending on this number. After returning there is a branch instruction to the address that is in register 14.
Now, the loaded the program memory (Flash) will contain the SoftDevice in addition to our application code:
+-----------------------+ Size of Flash
| Application |
| |
| |
| |
| |
+-----------------------+
| App Vector Table | APP_CODE_BASE
+-----------------------+
| SoftDevice |
| |
| |
+-----------------------+
|SoftDevice Vector Table| 0x00001000
+-----------------------+
| Master Boot Record |
+-----------------------+
| MBR Vector Table | 0x00000000
+-----------------------+
So notice that the Application has a vector table, as does the SoftDevice, and
also the master boot record. When an exception is triggered then the mbr
boot record's SVCHandler will be called which will delegate to the softdevice
SVCHandler depending on the number passed to svc
.
All interrupts are routed througt the MBR and the SoftDevice. The Supervisor Call (SVC) interrupt is always intercepted by the SoftDevice regardless of whether it is enabled or not. The SoftDevice inspects the SVC number, and if it is equal or greater than 0x10, the interrupt is processed by the SoftDevice. SVC numbers below 0x10 are forwarded to the application's SVC interrupt handler.
The source for the SVC_handler seems to be in
components/libraries/svc/nrf_svc_handler.c
:
void __attribute__((naked)) SVC_Handler(void)
{
__ASM volatile
(
"tst lr, #4\t\n" // Test bit 2 of EXT_RETURN to see if MSP or PSP is used
"ite eq\t\n" //
"mrseq r0, MSP\t\n" // Move MSP into R0.
"mrsne r0, PSP\t\n" // Move PSP into R0.
"b nrf_svc_handler_c\t\n" // Call C-implementation of handler. Exception stack frame in R0
".align\t\n" // Protect with alignment
);
}
So this would then be set as entry 11
in the SoftDevice's interrupt vector
table and called when svc calls is issued.
Looking at the above assembly there is first a test using tst
using the link
(lr) register. Next we have the it
instruction, which is like if-then and then
and can be ITT
for if then
, or ITE
for if else
, and the two instruction
following this instruction is the block for the if-then. So depending on the
test either MSP (Main Stack Pointer) or PSP (Processor Stack Pointer) will be
placed in r0 which is the argument to nrf_svc_handler_c
.
void nrf_svc_handler_c(uint32_t* p_svc_args) {
This function will iterate over all the function pointers that have been
specified using the section .svc_data
. A function can be registered using a
macro NRF_SVC_FUNCTION_REGISTER
:
#define NRF_SVC_FUNCTION_REGISTER(svc_number, name, func) \
STATIC_ASSERT(svc_number != 0); \
NRF_SECTION_ITEM_REGISTER(svc_data, nrf_svc_func_reg_t const name) = \
{ \
.svc_num = svc_number, \
.svci_num = NRF_SVCI_SVC_NUM_INVALID, \
.func_ptr = (nrf_svc_func_t)func \
}
#define NRF_SECTION_ITEM_REGISTER(section_name, section_var) \
section_var __attribute__ ((section("." STRINGIFY(section_name)))) __attribute__((used))
typedef struct
{
uint32_t svc_num; /**< Supervisor call number (actually 8-bit, padded for alignment). */
uint32_t svci_num; /**< Supervisor call indirect number. */
nrf_svc_func_t func_ptr;
} nrf_svc_func_reg_t
(gdb) x/20xw 0x0
0x0: 0x20000400 0x00000a81 0x00000715 0x00000a61
0x10: 0x0000071f 0x00000729 0x00000733 0x00000000
0x20: 0x00000000 0x00000000 0x00000000 0x00000aa5
0x30: 0x0000073d 0x00000000 0x00000747 0x00000751
0x40: 0x0000075b 0x00000765 0x0000076f 0x00000779
And entry 11 should be the SVC_Handler.
(gdb) x/xw 0x00000000 + (4*11)
0x2c: 0x00000aa5
(gdb) x/9i 0x00000aa5
0xaa5: tst.w lr, #4
0xaa9: ite eq
0xaab: mrseq r1, MSP
0xaaf: mrsne r1, PSP
0xab3: ldr r0, [r1, #24]
0xab5: subs r0, #2
0xab7: ldrb r0, [r0, #0]
0xab9: cmp r0, #24
0xabb: bne.n 0xac4
And this matches the assembly code we saw above for the SoftDevice SVC_Handler.
Alright, so if we are interested in inspecting the code that get executed
for this function we would need to find the function that was registered
for SD_BLE_GAP_DEVICE_NAME_SET
which would have been in a section named
.svc_data in the original object file. But this was then transformed into
softdevice/s132/hex/s132_nrf52_7.2.0_softdevice.hex so we don't have any
information about sections only addresses, and the data that goes into those
addresses.
But how about setting a break point in the SVC_Handler and trying to follow the calls down from there:
(gdb) br main.c:main
(gdb) set $primask = 1
(gdb) br *0xaa5
(gdb) c
(gdb) set $primask = 0
(gdb) c
Setting primask
to 1 prevents the activation of all exceptions with a
configurable priority.
work in progress
Logs can be viewed using JLinkRTTViewer:
$ JLinkRTTViewer
And we can add log statement using:
NRF_LOG_INFO("main....");
RTT stands for Real Time Transfer.
Task are registers just like events. Writing to a task register will start some task (whatever that might mean to some module in the system that is interested (reads the task register value).
The events register are like status registers
and reading a 1
means that a particular event has occurred. The event can be
cleared by writing 0
.
Is is way of having some instructions/code be run when there is a specific input on a GPIO pin. This is similar to interrupts but the difference is that interrupts will stop the currently executing code, and then resume after the interrupt handler has been run.
A Task in this context is a peripheral register. Writing a 1 to an entry in this register will cause the peripheral to do something. These registers are readonly. So in this case writing to the register is what triggers the Task, but a Task can also be triggered by an event happening.
An Event is a peripheral register which is used to indicate to the
firmware/hardware that an event has happend. For example, this could be an GPIO
input pin going from low to high. This is like an interrupt and these are
enabled using the INTENSET
register and disabled using INTENCLR
.
So we can connect a GPIO pin to a GPIOTE channel and configure it to generate an interrupt on the GPIO pin state changes.
GPIOTE is a peripheral as well which has 8 channels as mentioned above. Each channel can be configured to connect to a GPIO pin. If a channel is connected to a GPIO input an interrupt can be trigger when a state change happends. If a channel is connected to a GPIO output pin, it can control its output by setting it high/low by writing to the GPIOTE register.
Is a way of accessing GPIO pins using tasks and events. So a GPIOTE channel would be associated with a pin and we can enable it so that when a state change occurs a task will automatically created.
How this works is that we configure a register named CONFIG[n] (where n is
between 0 and 7) and specify which port/pin to connect (the channel?) to.
There are more options that can be configured and there is an example in
led-external-gpiote.s which shows the other
options available. After this we can write to this channel (I'm really not sure
if I'm using the right terminology here but hopefully this makes sense just the
same) using the register TASKS_OUT[n]
or TASKS_SET[n]
.
There are 8 GPIOTE channels and up to 3 tasks can be configured for each
channel. There are two fixed tasks which are SET
and CLR
, and OUT
which
can be configured to be one of Set
, Clear
, or Toggle
.
Each channel can generate events for the following triggers:
- Rising Edge
- Falling Edge
- Any change
- Real Time Counter (RTC) Low power and low frequency
- Timer High power and high frequency
CONFIG0 to CONFIG7 registers configure the channels:
- Mode: Event (input pin) or Task mode (output pin).
- GPIO pin: the pin that is to be connected to the channel.
- Initial value for pin in Task mode.
- Operation when in Task mode for the OUT task.
- Configure the operation that that will trigger an IN event when in Event mode.
EVENTS_IN0 to EVENTS-7 are 32 bit registers that are updated when an event happens in one of the GPIOTE channels.
TASKS_SET0 to TASKS_SET7 are used for GPIO output pins that are connected to a GPIOTE channel. If we write to one of these registers it will place the output port to high.
These are standalone drivers for nrf peripherals which were originally in the nRF5 SDK and have been extracted to standalone modules/libraries. https://github.com/NordicSemiconductor/nrfx
00> <error> app: RNG context cannot be allocated on the stack.
00>
00> <error> nrf_ble_lesc: nrf_crypto_init() returned error 0x8515.
00>
00> <error> peer_manager: pm_init failed because sm_init() returned Unknown error code.
00>
00> <error> app: Fatal error
This is most likly due to not having enabled `NRF_CRYPTO_RNG_STATIC_MEMORY_BUFFERS_ENABLED in config/sdk_config.h:
#ifndef NRF_CRYPTO_RNG_STATIC_MEMORY_BUFFERS_ENABLED
#define NRF_CRYPTO_RNG_STATIC_MEMORY_BUFFERS_ENABLED 1
#endif
This section will take a look at how GPIOTE interrupts work in embassy-nrf.
If we take a look at the file src/gpiote.rs
we can find the interrupt
handler (I've removed some of the cfg conditional features to simply this):
#[interrupt]
fn GPIOTE() {
unsafe { handle_gpiote_interrupt() };
}
And handle_gpiote_interrupts()
looks like this:
unsafe fn handle_gpiote_interrupt() {
let g = regs();
for i in 0..CHANNEL_COUNT {
if g.events_in[i].read().bits() != 0 {
g.intenclr.write(|w| w.bits(1 << i));
CHANNEL_WAKERS[i].wake();
}
}
if g.events_port.read().bits() != 0 {
g.events_port.write(|w| w);
let ports = &[&*pac::P0::ptr(), &*pac::P1::ptr()];
for (port, &p) in ports.iter().enumerate() {
let bits = p.latch.read().bits();
for pin in BitIter(bits) {
p.pin_cnf[pin as usize].modify(|_, w| w.sense().disabled());
PORT_WAKERS[port * 32 + pin as usize].wake();
}
p.latch.write(|w| w.bits(bits));
}
}
}
First, regs()
will return (simplifed a little):
fn regs() -> &'static pac::gpiote::RegisterBlock {
unsafe { &*pac::GPIOTE::ptr() }
}
So this will return a pointer to a RegisterBlock and stores that in variable g
for GPIOTE I think. Lets take a closer look at
RegisterBlock:
pub struct RegisterBlock {
...
pub events_in: [crate::Reg<events_in::EVENTS_IN_SPEC>; 8],
_reserved4: [u8; 0x5c],
#[doc = "0x17c - Event generated from multiple input GPIO pins with SENSE mechanism enabled"]
pub events_port: crate::Reg<events_port::EVENTS_PORT_SPEC>,
_reserved5: [u8; 0x0184],
...
}
So events_in
is an array of size 8 (8 channels) and the type that this array
stores is events_in::EVENTS_IN_SPEC>
. The for loop it is iterating over the
GPIOTE channels (8 of them) and for each channel it will check if the
events_in_x
has any bits set, and if the channel does disable the interrupt by
writing to INTENCLR. After that it will call the waker for the current channel.
Next, we have the events_port
which is about the nrf SENSE
feature which is
similar to events (see GPIO SENSE section for details). EVENTS_PORTS
is a 32
bit register with an entry for each pin, so the above code is checking if any
value is set in that register (otherwise it would be 0). Next, events_ports
will be cleared. Then all the ports (two on some nrfs) and iterated over.
Now, the next part is where the LATCH
register is read which provides
information about which pin(s) triggered the DETECT
signal to be set. This
will return a bit patterns of the register. This bit patterns will be used
to create a BitIter:
struct BitIter(u32);
impl Iterator for BitIter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
match self.0.trailing_zeros() {
32 => None,
b => {
self.0 &= !(1 << b);
Some(b)
}
}
}
}
What next
is doing is that it is is working backwards through a binary number
starting from the least significant bit and only returning the bits that have
are set. For example if we have 101
this function will first return 0
as
that bit position was set, after that self.0 will be 100 and the next call will
return 2 as that is the next bit that is set. So this will only perform the
following for bits that were set:
for pin in BitIter(bits) {
p.pin_cnf[pin as usize].modify(|_, w| w.sense().disabled());
PORT_WAKERS[port * 32 + pin as usize].wake();
}
And this is disableing SENSE for the bit, and then calling wake on the port wakers.
This is a feature of gpio pins which can be used for detecting/sensing change to the pins state. This sounds similar to events but I think that events are also hooked into the tasks and PPI which might be the difference.
Each GPIO Pin can be configured using the PIN_CNF_x registers (0..31).
- Direction
- Drive strength
- Enabing pull up/down reistors.
- Pin sensing
- Input buffer disconnect
- Analog input
The sence feature allows the pin to detect either a high or low level on the pins input. If enabled and there is a change to high/low then the DETECT signal will be set high. The DETECT signal will be set if any GPIO pin which has sense enabled has been triggered. In addition the LATCH register will be updated and the pin that triggered the change will be set.