-
Notifications
You must be signed in to change notification settings - Fork 0
Arduino pulsebox code structure
An empty Arduino .ino
source code file looks like this:
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
The setup()
function is used for Arduino configuration, whereas the loop()
function is intended for code that is run on repeat indefinitely. As we will see later, we will not use the loop()
function and instead define our own function sequence()
for the pulse sequence.
First we show the complete setup()
block for triggered and continuous/repeat mode of pulsebox operation and then we discuss its key parts in detail:
/* Triggered mode */
void setup() {
for (int i=1; i<=78; i++)
detachInterrupt(i);
void __disable_irq(void);
REG_PIOC_OER = 0b11111101001111111110;
REG_PIOC_OWER = 0b11111101001111111110;
attachInterrupt(52, sequence, RISING);
}
/* Continuous/repeat mode */
void setup() {
for (int i=1; i<=78; i++)
detachInterrupt(i);
void __disable_irq(void);
REG_PIOC_OER = 0b11111101001111111110;
REG_PIOC_OWER = 0b11111101001111111110;
while(1) {
sequence();
delay(1);
}
}
A pulsebox is an application where timing accuracy is critical. However, if the Arduino received an interrupt during a pulse sequence, other code would be executed immediately and the timing of the sequence would be thrown off. To prevent this, we disable interrupts. First we iterate through all numbered Arduino pins and detach any possible interrupts:
for (int i=1; i<=78; i++)
detachInterrupt(i);
As a next step we disable interrupts in this way:
void __disable_irq(void);
Note: whether or not the second step is redundant was not tested. During development and testing, no effect of an interrupt was observed.
Arduino Due has 79 numbered pins, from 0 to 78. The Arduino pin mapping table in the Arduino documentation shows their function. Any pin labeled 'Digital Pin' can be used as a pulsebox pin. The pin n
could be set to output with the Arduino function:
pinMode(n, OUTPUT);
In principle, we could iterate over all pulsebox pins and set them to OUTPUT
. We will choose a different approach, which will be especially advantageous later when we discuss writing into the digital pins.
The Atmel SAM3X8E MCU (on which Arduino Due is built) divides its pin IO (PIO) into distinct PORTs: A, B, C, and D. Consulting the Arduino pin mapping table reveals for example that the Arduino pin 0 is internally pin 8 of PORT A: PA8. Similarly, Arduino pin 1 is mapped to PA9. However, Arduino pin 2 belongs to PORT B: PB25.
Each SAM3X8E PIO PORT has numerous 32-bit registers, where n-th bit of the register corresponds to the n-th pin of the given PORT. The REG_PIOC_OER
is the output enable register for PORT C. If we decide that all the pulsebox pins belong to PIO PORT C (as is the case with this pulsebox implementation), we can set all relevant pins to OUTPUT
mode with one instruction just by setting the appropriate REG_PIOC_OER
bits to 1:
REG_PIOC_OER = 0b11111101001111111110;
Then we use the REG_PIOC_OWER
output write enable register to enable writing to the same pins:
REG_PIOC_OER = 0b11111101001111111110;
See Pulsebox channel to pin mappings for the reasoning behind the specific PORT and pin choice.
In this case we attach an interrupt to a chosen Arduino pin we will call the trigger pin. When a rising edge (e. g. change from the LOW
state to HIGH
state) occurs on the trigger pin, the pulse sequence, described in the sequence()
function, is produced.
attachInterrupt(trigger_pin, sequence, RISING);
The trigger_pin above is specified in Arduino notation and should be between 0 and 78. The trigger pin should not be identical to any pulsebox pins.
Here we simply use an endless loop to run the sequence()
function. An optional delay is inserted between the consecutive runs of sequence()
. This delay is facilitated by the Arduino delay(n)
function, which produces a delay of n milliseconds:
while(1) {
sequence();
delay(delay_milliseconds);
}
Note: If more accurate delay between sequences is needed, a different approach to delay should be employed.
The contents of the sequence()
function can be understood from this trivial example of a single-channel, single-pulse sequence:
void sequence() {
REG_PIOC_ODSR = 0b10; /* pulsebox channel 1 (PORT C pin 2) HIGH */
asm volatile (
"MOVW R1, #0xa\n" /* 10 iterations */
"MOVT R1, #0x0\n"
"LOOP0:\n\t"
"NOP\n\t"
"SUB R1, #1\n\t"
"CMP R1, #0\n\t"
"BNE LOOP0\n"
);
REG_PIOC_ODSR = 0b0; /* pulsebox channel 1 (PORT C pin 2) LOW */
}
Any multi-channel digital pulse sequence can be broken down into two parts (or events):
-
State change: when the state (
HIGH
/LOW
) of one or more output channels is changed - Delay: A period of time between state changes
Here we use the low-level SAM3X8E register REG_PIOC_ODSR
to enable changing the status of all the pulsebox pins at once. For example, to set channels 1 and 2 to HIGH
, we use the following C
statement:
REG_PIOC_ODSR = 0b101;
For our example, channel 1 is mapped to PORTC
bit 1 and channel 2 to bit 3, according to the Pulsebox channel to pin mappings.
If our pulsebox code consisted only of state change code blocks, we would find that the actual state changes, when measured on an oscilloscope, would be separated by a two-clock interval (for the default 84 MHz master clock of Arduino Due, this is about 24 ns). If we wish to have more control over the delay between state changes, we need to add a delay code block. Here we present two approaches, each suitable for a different scenario.
By repeatedly using the NOP
assembly instruction between two output changes, it is possible to increment the delay by one-clock steps (12 ns for the default Arduino clock):
REG_PIOC_ODSR = 0b1; /* set channel 1 to HIGH */
asm volatile("NOP\n"); /* + 12 ns */
asm volatile("NOP\n"); /* + 12 ns */
asm volatile("NOP\n"); /* + 12 ns */
REG_PIOC_ODSR = 0b0; /* set channel 1 to LOW */
With three NOP
instructions used in the example, we added three clocks (36 ns) to the always present two-clock (24 ns) interval, making the total duration between output changes 24 ns + 36 ns = 60 ns.
For longer delays, a certain number of iterations through a delay loop is performed. The higher the iteration count, the longer the delay.
The delay loop can be understood as an empty C
for
loop:
REG_PIOC_ODSR = 0b1; /* set channel 1 to HIGH */
int iters = 10; /* we will iterate the loop 10 times */
int i; /* we will hold the current iteration count here */
for(i = 0; i < iters; i++) {
; /* do nothing
}
REG_PIOC_ODSR = 0b0; /* set channel 1 to LOW */
It should be noted that the C
compiler will try to optimize the code and eliminate parts it considers unnecessary. As nothing effectively happens in the loop above, there is a significant chance that it will be eliminated and the desired delay will not be produced. This was the result of our testing, even after changing the optimization flags of the gcc
compiler.
Our approach relies on the inline assembly implementation of the example above:
REG_PIOC_ODSR = 0b1; /* set channel 1 to HIGH */
asm volatile (
"MOVW R1, #0x14\n" // 20 iterations, approx 1.2 microsec delay
"MOVT R1, #0x0\n"
"LOOP0:\n\t"
"NOP\n\t"
"SUB R1, #1\n\t"
"CMP R1, #0\n\t"
"BNE LOOP0\n"
);
REG_PIOC_ODSR = 0b0; /* set channel 1 to LOW */
This loop, step by step, does the following:
-
Setting the 32-bit iteration variable into a general-purpose register
R1
. This is done in two steps. The lower 16 bits are stored usingMOVW R1, #0x14
and the upper 16 bits withMOVT R1, #0x0
. In our example, we wish to set the iteration count to 20, which is0x14
in hexadecimal. The lower 16 bits are0x0014
and the upper 16 bits are0x0000
. -
The delay loop
2.1. Starting the loop.
LOOP0:
is the loop label. After the end of each loop iteration, program execution returns back here, until the number of performed iteration matches the desired iteration count.2.2. Do nothing.
NOP
is a no-operation instruction.2.3. Decrement the iteration count by one:
SUB R1, #1
2.4. Check if iteration counter reached zero:
CMP R1, #0
2.5. If the iteration counter is non-zero, go back to the start of the loop:
BNE LOOP0
-
End of the loop: If the value of the iteration counter was zero in step 2.5., the loop finishes.
-
When using multiple
ASM
loops in one.ino
source code you must ensure that each delay loop has a unique label. We choose to use theLOOP#
format for the loop labels, with the#
being a unique number starting from zero and incrementing as loops are added. -
Limiting the iteration count variable to 16 bits can get around the process of writing into the
R1
register in two steps. In such case it is possible to use only theMOVW
orMOV
instruction. -
Alternative
ASM
loops have been experimented with during development. Choosing a differentASM
loop can have two notable effects: (1) different minimal delay (e. g. delay obtained with a one-iteration loop), and (2) different delay granularity (e. g. the delay time increment accopmanied by increasing the iteration count by one). TheASM
loop presented above was chosen because its linearity — changing the iteration count from 1 to 2 prolonged the delay by the same amount as when going from 100.000 to 100.001 iterations. This was not necessarily observed for the other variants, where linearity was broken, especially for very low iteration counts. Still, the alternativeASM
delay loops are presented below:
/* no-NOP loop */
asm volatile(
"MOVW R1, #0x14\n"
"MOVT R1, #0\n"
"LOOP0:\n\t"
"SUB R1, #1\n\t"
"CMP R1, #0\n\t"
"BNE LOOP0\n"
);
/* SUBS, no-NOP loop */
asm volatile(
"MOVW R1, #0x14\n"
"MOVT R1, #0\n"
"LOOP0:\n\t"
"SUBS R1, #1\n\t"
"BNE LOOP0\n"
);
/* SUBS, no-NOP, 16-bit only loop */
asm volatile(
"MOV R1, #0x14\n"
"LOOP0:\n\t"
"SUBS R1, #1\n\t"
"BNE LOOP0\n"
);
The manual for the used MCU---with details on the ASM
instruction set---is a useful reference when playing around with the ASM
delay loops. Arduino Due is based on the Atmel SAM3X8E MCU. Here is the link to the datasheet. The instruction set summary starts on page 81.
The following working .ino
code produces a simple single-channel, single-pulse sequence running repeatedly (in the continuous mode) with a delay of 1 ms between repetitions.
void setup() {
for (int i=1; i<=78; i++)
detachInterrupt(i);
void __disable_irq(void);
REG_PIOC_OER = 0b11111101001111111110;
REG_PIOC_OWER = 0b11111101001111111110;
while(1) {
sequence();
delay(1);
}
}
void sequence() {
REG_PIOC_ODSR = 0b1010;
asm volatile (
"MOVW R1, #0xa\n"
"MOVT R1, #0x0\n"
"LOOP0:\n\t"
"NOP\n\t"
"SUB R1, #1\n\t"
"CMP R1, #0\n\t"
"BNE LOOP0\n"
);
REG_PIOC_ODSR = 0b0;
}
void loop() {
;
}
TODO: link to codeblocks