Here is another take on the same thing.
gcc-avr supports bit fields. This allows value to be directly assigned to a bit.
typedef struct {
unsigned char B0: 1;
unsigned char B1: 1;
unsigned char B2: 1;
unsigned char B3: 1;
unsigned char B4: 1;
unsigned char B5: 1;
unsigned char B6: 1;
unsigned char B7: 1;
} BIT8_T;
//extend bit fields
#define PORTBbits (*(volatile BIT8_T *)&PORTB)
#define DDRBbits (*(volatile BIT8_T *)&DDRB)
//helper functions. don't call directly
#define _DDR(id, pin) DDR ## id ## bits.B ## pin
#define _PORT(id, pin) PORT ## id ## bits.B ## pin
//port functions
#define DDR(pin) _DDR(pin)
#define PORT(pin) _PORT(pin)
PORTBbits and DDRBbits are bit fields of their whole byte countparts - you can expand them easily to cover other registers, like PINx.
DDR() and PORT() functions are actually what we want. _DDR() and _PORT() are there to defeat the compiler so don't use them.
If you define a pin like this:
#define HC595_SCK B, 0 //hc595_sck on portb.0
You can later directly assign values to it, like this:
#define HC595_SCK B, 0 //hc595_sck on portb.0
DDR(HC595_SCK) = 1; //hc595_sck as output
do {
PORT(HC595_SCK) = 0; //clear sck
...
PORT(HC595_SCK) = 1; //set sck
}
...
PORT(HC595_SCK) ^= 1; //toggle hc595_sck
To some, this may be more intuitive. But it is slightly slower, due to the bit fields.