(Have I mentioned how much I hate the #hashtags Dave added to the forums, yet? A lot.
Just to un-b0rk the preprocessor directives in code snippets, I have to prepend the first line with a non-breaking space, U+00A0.
Not to say the stupid "suggestions" the editor pops up, when you type a #. This is just evil.
)
I recently looked at SAM4S4/SAM4S2 in 48-pin LQFP, which has two GPIO ports (with max. 32 pins each), A and B. Port A offset is 0x400E0E00, and port B 0x400E1000. Other SAM4S have port C at 0x400E1200 and port D at 0x400E1400.
GPIO input/output control:
- Each bit in PIO_OSR (offset 0x0018) corresponds to a pin. If zero, the pin is an input; otherwise, the pin is an output.
- Writing PIN_OER (0x0010), each set bit sets the pin to an output (setting the corresponding bit in PIO_OSR).
- Writing PIN_ODR (0x0014), each set bit sets the pin to an input (clearing the corresponding bit in PIO_OSR).
- Each bit in PIO_PSR (0x0008) determines whether a pin is a GPIO pin (set), or used for a peripheral (clear).
- Each bit pair in PIO_ABCDSR1 (0x0070) and PIO_ABCDSR2 (0x0074) for a pair of bits, that select between one of the peripherals for that pin, if PIO_PSR is zero for that pin.
GPIO output state control:
- Each bit set in PIO_ODSR (0x0038) reflects the set output pin states. You can write to this register to set all (max 32) output pins to specific states.
- Each bit set in PIO_SODR (0x0030) sets the corresponding output pin high.
- Each bit set in PIO_CODR (0x0034) sets the corresponding output pin low.
- PIO_OWSR (0x00A8) is a read-only mask of output pins affected by writes to PIO_ODSR. It defaults to all zeros.
If a bit is clear/zero, that bit in PIO_ODSR does affect the output pin state. If a bit is one, the output pin state is unchanged by writes to PIO_OWSR. - You can set bits (disable PIO_ODSR from affecting the corresponding pins) by writing to PIO_OWER (0x00A0).
- You can clear bits (enable PIO_ODSR from affecting the corresponding pins) by writing to PIO_OWDR (0x00A4).
Let's say you are using PA00..PA8 (first nine pins of port A) for some parallel communication task, and want a function that sets them, in parallel, to a specific pattern. However, some of the other pins in port A are also GPIO outputs, and you don't want to affect those.
#include <stdint.h>
#define PIOA_ODSR_ADDR 0x400E0EA0 /* RW */
#define PIOA_OWSR_ADDR 0x400E0EA8 /* R */
#define PIOA_OWDR_ADDR 0x400E0EA4 /* W */
#define PIOA_OWER_ADDR 0x400E0EA0 /* W */
static inline void set_a00_a08(unsigned int value)
{
uint32_t old_owsr;
/* Save old mask. */
old_owsr = *(volatile uint32_t *)PIOA_OWSR_ADDR;
/* Enable writes to pins 0..8, disable to 9..15. */
*(volatile uint16_t *)PIOA_OWDR_ADDR = 0xFE00;
*(volatile uint16_t *)PIOA_OWER_ADDR = 0x01FF;
/* Update output pins */
*(volatile uint16_t *)PIOA_ODSR_ADDR = value;
/* Restore old mask. */
*(volatile uint32_t *)PIOA_OWDR_ADDR = old_owsr;
*(volatile uint32_t *)PIOA_OWER_ADDR = ~old_owsr;
}
Note that if you have room for a look-up table of 2+2
N words in ROM/flash, 32 bits each, you can use any set of N pins in any order of significance within a single port (A, B, C, or D). For 9 pins, that is just 2056-byte look-up table.
(Why 9? Because it is one option for many LCD displays using a parallel data interface. I looked at SAM4S as a possible low-cost "blitter" for 320x240 or 480x320 18-bit color LCDs.)
A variant of
digitalWriteFast() implemented by an Arduino library and Teensyduino, that takes a pin number (using a mapping of your choice, can mix across all ports in any order), and the state it should be set to, on SAM4S can be written as:
#include <stdint.h>
#define PIOA_CODR_ADDR 0x400E0E34 /* W */
#define PIOB_CODR_ADDR 0x40010E34 /* W */
#define PIOC_CODR_ADDR 0x40012E34 /* W */
#define PIOD_CODR_ADDR 0x40014E34 /* W */
#define PIOA_SODR_ADDR 0x400E0E30 /* W */
#define PIOB_SODR_ADDR 0x400E1030 /* W */
#define PIOC_SODR_ADDR 0x400E1230 /* W */
#define PIOD_SODR_ADDR 0x400E1430 /* W */
/* Pin port mapping, CODR and SODR addresses for the port, and the bit mask. */
static const uint32_t _pin_odr[PIN_COUNT][3] = {
/* { PIOx_CODR_ADDR, PIOx_SODR_ADDR, 1<<0 }, */
};
void digitalWriteFast(unsigned int pin_number, unsigned int state)
{
#ifdef CHECK_ARGS
if (pin_number < PIN_COUNT)
*(volatile uint32_t *)(_pin_odr[pin_number][!!state]) = _pin_odr[pin_number][2];
#else
*(volatile uint32_t *)(_pin_odr[pin_number][state]) = _pin_odr[pin_number][2];
#endif
}
depending on whether you know your arguments are always safe or not. Note that this does not interfere with parallel writes through ODSR.
If you wanted a function that sets a number of output pins to different states as simultaneously as possible (i.e., port A first at once, then port B pins, and so on), you can use gcc vector extensions:
#include <stdint.h>
/* Pin number mapping. Each row has exactly one bit set. */
static const uint32_t _pin_bits[PIN_COUNT][4] = {
/* { port_A_bitmask, port_B_bitmask, port_C_bitmask, port_D_bitmask }, */
};
typedef uint32_t uint32x8 __attribute__((__vector_size__ (64)));
#define SET_PIN(number) ((uint32x8){ _pin_bits[number][0], _pin_bits[number][1], _pin_bits[number][2], _pin_bits[number][3], _pin_bits[number][0], _pin_bits[number][1], _pin_bits[number][2], _pin_bits[number][3] })
#define CLEAR_PIN(number) ((uint32x8){ _pin_bits[number][0], _pin_bits[number][1], _pin_bits[number][2], _pin_bits[number][3], 0, 0, 0, 0 })
#define PIN(number, state) ((uint32x8){ _pin_bits[number][0], _pin_bits[number][1], _pin_bits[number][2], _pin_bits[number][3], \
(!!state) ? __pin_bits[number][0] : 0, \
(!!state) ? __pin_bits[number][1] : 0, \
(!!state) ? __pin_bits[number][2] : 0, \
(!!state) ? __pin_bits[number][3] : 0 })
static inline void digitalWriteFastMany(const uint32x8 config)
{
uint32_t olda_owsr, oldb_owsr, oldc_owsr, oldd_owsr ;
/* Save old masks. */
olda_owsr = *(volatile uint32_t *)PIOA_OWSR_ADDR;
oldb_owsr = *(volatile uint32_t *)PIOB_OWSR_ADDR;
oldc_owsr = *(volatile uint32_t *)PIOC_OWSR_ADDR;
oldd_owsr = *(volatile uint32_t *)PIOD_OWSR_ADDR;
/* Disable writes to all pins; enable our mask pins. */
*(volatile uint32_t *)PIOA_OWDR_ADDR = ~(uint32_t)0;
*(volatile uint32_t *)PIOA_OWDR_ADDR = config[0];
*(volatile uint32_t *)PIOB_OWDR_ADDR = ~(uint32_t)0;
*(volatile uint32_t *)PIOB_OWDR_ADDR = config[1];
*(volatile uint32_t *)PIOC_OWDR_ADDR = ~(uint32_t)0;
*(volatile uint32_t *)PIOC_OWDR_ADDR = config[2];
*(volatile uint32_t *)PIOD_OWDR_ADDR = ~(uint32_t)0;
*(volatile uint32_t *)PIOD_OWDR_ADDR = config[3];
/* Update output pins */
*(volatile uint32_t *)PIOA_ODSR_ADDR = config[4];
*(volatile uint32_t *)PIOB_ODSR_ADDR = config[5];
*(volatile uint32_t *)PIOC_ODSR_ADDR = config[6];
*(volatile uint32_t *)PIOD_ODSR_ADDR = config[7];
/* Restore old masks. */
*(volatile uint32_t *)PIOA_OWDR_ADDR = olda_owsr;
*(volatile uint32_t *)PIOA_OWER_ADDR = ~olda_owsr;
*(volatile uint32_t *)PIOB_OWDR_ADDR = oldb_owsr;
*(volatile uint32_t *)PIOB_OWER_ADDR = ~oldb_owsr;
*(volatile uint32_t *)PIOC_OWDR_ADDR = oldc_owsr;
*(volatile uint32_t *)PIOC_OWER_ADDR = ~oldc_owsr;
*(volatile uint32_t *)PIOD_OWDR_ADDR = oldd_owsr;
*(volatile uint32_t *)PIOD_OWER_ADDR = ~oldd_owsr;
}
In this case, you could clear pin 15 and set pin 11 using
digitalWriteFastMany(PIN(15,0) | PIN(11,1)) or
digitalWriteFastMany(CLEAR_PIN(15) | SET_PIN(11)). This works, because of the gcc support for the vector attribute and trivial arithmetic operations on them.
While the digitalWriteFastMany() code is not optimal, it does what it says on the tin (except a possible latency, loading the
config[4..7] value between updates to port B, C, and D). To avoid that, one could easily write it in gcc inline assembly for each particular instruction set, minimizing the latency between port changes.
The above shows that you usually
do not want a variable number of arguments, and can instead use alternate ways to define multiple items at once.
Let's say you want a function that modifies output pins sequentially, and these sequences can be arbitrarily long. C strings come to mind here:
/* Maximum number of pins = 127.
HH = PIN_RESERVED ^ ((bit & 31) + 32*(port) + 128*(!state))
where PIN_RESERVED = ((bit & 31) + 32*(port) + 128*(!state)) of an unused pin,
and bit = 0..31, state = 0 or 1, and port = 0..3.
*/
#define PIN_RESERVED 0xB7 /* Port B pin 23 cannot be cleared. */
#define PIN_1_SET "\xB7" /* Port A pin 0 */
#define PIN_2_SET "\xB6" /* Port A pin 1 */
#define PIN_3_SET "\xF2" /* Port C pin 5 */
/* : */
#define PIN_1_CLEAR "\x37" /* Port A pin 0 */
#define PIN_2_CLEAR "\x36" /* Port A pin 1 */
#define PIN_3_CLEAR "\x72" /* Port C pin 5 */
static inline void pin_sequence(const unsigned char *pins)
{
while (*pins) {
const uint32_t p = (*pins++) ^ PIN_RESERVED;
/* Note: ((p>>5)&3)*32 == p & 0x60, and ((p >> 7) & 1)*4 == (p & 0x80) >> 5. */
*(volatile uint32_t *)(0x400E0E30 + (p & 0x60) + ((p & 0x80) >> 5)) = ((uint32_t)1) << (p & 31);
}
}
As you can see, there are many interface approaches to choose from. Picking one depends on exactly what you want to do; the above are just examples for SAM4S.