Hardware specs
- Resolution (granularity): 24 ns
- The shortest pulse or delay: 24 ns
- The longest pulse or delay: 255 s
- Number of channels: 32
- Linear dynamic range: 24 ns – 255 s
- Run-to-run uncertainty: 100 ps peak-to-peak (given by external clock)
- Event capacity: approx 20,000 pulses
- Price: below USD 100 including connectors and a case (+price of an optional external clock generator)
Output channels and trigger pins
The Arduino pins for the output channels are chosen so that they all belong to a single PIO PORT (PORTC
)
channel | PORTC bit | Arduino pin | channel | PORTC bit | Arduino pin |
---|---|---|---|---|---|
1 | 1 | 33 | 9 | 12 | 51 |
2 | 3 | 35 | 10 | 2 | 34 |
3 | 5 | 37 | 11 | 4 | 36 |
4 | 7 | 39 | 12 | 6 | 38 |
5 | 9 | 41 | 13 | 8 | 40 |
6 | 18 | 45 | 14 | 19 | 44 |
7 | 16 | 47 | 15 | 17 | 46 |
8 | 14 | 49 | 16 | 15 | 48 |
Trigger channel: Arduino pin 52
The primitive code blocks: output changes and artificial delays
The output change is made by writing into the REG_PIOC_ODSR
register. For example, to set channels 1 and 2 to HIGH
, we use the following C
statement:
REG_PIOC_ODSR = 0b101;
The REG_PIOC_ODSR
bits are mapped to the pulsebox channels according to the pin mapping table shown above. For our example, channel 1 is on bit 1, and channel 2 on bit 3.
Also note that this example is an illustration. In our software implementation, we always write out the full 32-bit value to be written to REG_PIOC_ODSR
. In the example, we were concerned with two channels with respective REG_PIOC_ODSR
bits 1 and 3, so we used a three-bit value of 0b101
to set them both to HIGH
. If we now wanted to set them all to LOW
, we could write 0b0
. However, this would set all the channels to zero, which was probably not the intention. To avoid this, either use bit masks combined with the bitwise AND
, OR
, or XOR
operations to change only the desired bits of REG_PIOC_ODSR
, or always explicitly write all the 32 bits of REG_PIOC_ODSR
.
Without artificial delay, consecutive output changes occur within a two-clock interval (for the default 84 MHz master clock of Arduino Due, this is 24 ns). To modify the duration of the interval, artificial delay is needed. We have devised two ways of controlling the delay between output changes: the NOP
approach, and the delay-loop approach.
NOP delay
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.
Delay-loop delay
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 */
This example would likely not work, because by default, the C
compiler will try to eliminate useless code (which the delay loop most certainly is). 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 */
ASM
loop, step by step
The asm volatile(
begins theASM
loop."MOVW R1, #0x14\n"
moves the bottom 16 bits of the iteration count number to theR1
general-purpose register. The iteration count in this example is the hexadecimal value0x14
(20 in decimal)."MOVT R1, #0x0\n""
moves the upper 16 bits of the iteration count number to theR1
register."LOOP0:\n\t"
is a 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."NOP\n\t"
is a no-operation instruction."SUB R1, #1\n\t"
decrements the iteration count by one."CMP R1, #0\n\t"
compares the iteration count with zero."BNE LOOP0\n"
returns back to theLOOP0
loop label, unless the iteration count is zero.);
ends theASM
loop.
ASM
loops
Some important points about the - When multiple
ASM
delay loops are used throughout the.ino
source code, each loop needs to have its unique loop 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. - The desired iteration count value can be any 32-bit number (in hexadecimal,
0xffffffff
is the maximum). However, when the 32-bit value is moved to theR1
register, it needs to be moved in two steps: first, the lower 16-bit portion (withMOVW
), and then the higher 16-bit portion (withMOVT
). If the value is a 16-bit number, it is possible to ommit theMOVT
instruction and use only theMOVW
, orMOV
instruction.
ASM
loop
Alternative versions of the Throughout the development of the pulsebox, various ASM
loops have been devised. You are welcome to experiment with different versions. Differences in the delay-loop granularity, and shortest loop-based pulse, can be expected. Furthemore, when combined with individual NOP
instructions (for fine tuning the delay lengths), there can be some deviation from the trend of one-clock increments. This is most likely due to the MCU instruction prefetching, and/or scheduling. These deviations might also differ based on which version of the delay loop is used.
/* 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.
Connecting an external clock source
Locate the onboard quartz oscillator on your board. Remove it, along with the two capacitors on its sides. Four soldering points are left underneath the oscillator. Connect the central pin of a coaxial connector to the upper left one, and the ground of the connector to the lower left one.
The external clock signal should be a unipolar rectangular signal with 3.3 V amplitude and 50 % duty cycle. Its frequency should be in the 3 to 20 MHz range. 12 MHz is the default (the frequency of the onboard oscillator). This frequency is eventually multiplied by a factor of 7, which for a 12 MHz clock source leads to a 84 MHz master clock.