Es un poco cuestionable si algo como esto es una buena idea desde el principio, pero sí, puedes hacerlo. Comienza con el supuesto de que las direcciones para los bloques de registros PORTx están espaciadas uniformemente y lo mismo es cierto para los bloques SPIx. Esto parece ser cierto en las hojas de datos, pero tenga en cuenta que no está garantizado oficialmente.
De todos modos, a partir de este supuesto, puedes hacer algo como:
uint8_t n;
SPI_t *spi;
n = ((void*)portname - (void*)&PORTC) / ((void*)&PORTD - (void*)&PORTC);
spi = (SPI_t *) ((void *)&SPIC + n*((void *)&SPID - (void *)&SPIC));
...
spi->DATA = ...;
Con algunas macros puedes dejar que el preprocesador se encargue de toda esta aritmética, lo que sería mejor. Sin embargo, este fragmento de código no comprueba que la dirección resultante sea realmente válida y el módulo periférico existente. Y no puede utilizar este truco para obtener, por ejemplo, el número de vector ISR.
Calcularlo de otra manera: desde la dirección del módulo SPI hasta la dirección PORT, sería un poco más seguro, ya que para el módulo SPI válido siempre hay un puerto correspondiente. Tenga en cuenta también que para hacer que el código anterior sea más universal, se necesitaría al menos un #ifdef
de todos modos, para verificar si se define SPID
(XMEGA-E solo tiene un módulo SPI).