Можно написать драйвера, которые виртуализируют этот контроллер. И вместо обращения к регистрам происходит использование макросов и функций таких драйверов. Это хорошо, ибо можно написать симулятор контроллера, и отлаживать целевую программу на нем.
Более того, симулятор может принимать команды для контроллера, передавать их по каналу связи на железяку с контроллером, и выполнять их там. Обратно можно передавать ответы реального контроллера и данные. Это небыстро, но, зачастую, на начальном этапе освоения контроллера роль ошибок в документации и ошибок воприятия документаци куда важнее скорости. Лично я ненавижу такую процедуру: записал дрова, откомпилил, прошил, запустил, проверил - не в дугу. Написал чуть по другому, собрал, снова залили и т.д.
Но накладных расходов никто не отменял! Если вместо проверки бита готовности в регистре я буду вызывать некую функцию, то где-то это может стать просто фатально. Мощность современых контроллеров растет, но все же не хочется закладывать принципиально избыточные решения.
Можно пойти по пути макросов, но это сложно и не сильно универсально (макрос раскрывается либо в вызов симулятора, либо в набор операций с регистрами контроллера).
Есть более изящный способ!!!!
Берем кодогенератор типа Templarian
http://sourceforge.net/projects/templarian
В нем прописываем все "методы" объекта контроллера. Для разных вариантов - симулятор, "телепортация", "родное" исполнение кода.
В нужных местах проекта вставляем "выход" такого кодогенератора - и вуаля! Автоматическая модификация текста под целевую платформу готова!
Преимущества над макросами уже описал. Преимущества над #ifdef очевидны.
Если я ничего не путаю, в методиках использования С++ это называется "отделение интерфейса от реализации". Снова я кусок С++ "изобрел"

Продолжаем тему совершенствования работы с контроллерами.
Самое тупое, что бывает при освоении нового контроллера - это чтение линейной PDF документации на него. Т.е. ты вначале парсишь мозгами 10м файл, затем, осознав, из чего же состоит твой контроллер, уже немного начинаешь ориентироваться в нем.
Что касается самго процесса написания документации, то я так и вижу уходяшие к горизонту вереницы tecnical wrighters (надо полагать, прикованных за ногу к батарее), которые правят бесчисленные таблицы в документации какой-нибудь ATxmega. Хоть одна ошибка - и плеть надсмотрщика...
Мы пойдем другим путем.
Берем XML и строим иерархическую структуру типа:
* контроллер
* интерфейс контроллера
* регистр
* битовое поле (офсет и длина)
* зачения этого поля
Естественно, к каждой сущности приписано ее описание нормальным языком.
(я не считаю XML правильным или неправильным, я привел его как пример иерархической организации данных. Реализация идеи может быть сделана любым другим способом.)
Из этой структуры автоматически генерим "картинки с текстом" в виде графа сущностей контроллера. Так будет просто на порядки нагляднее линейнго описания.
Теперь берем программистский редактор, который воспринимает наше описание как... DTD! Выглядит этот так:
* жмем кнопарь - хочу работать с периферией
* далее выбираем : контроллер -> интерфейс -> регистр -> битовое поле -> значение
На каждом шаге мне автоматически подсовывается список того, из чего я могу выбрать в данный момент. И все это оформляется как "вызов" кодогенератора!
Размер исходника будет гораздо больше, но скорость восприятия несопоставимо выше. Мне не надо держать в голове всю информацию о регистрах. Мне нужно примерно помнить, как устроен контроллер, а далее "говорящие названия" все мне скажут.
Можно, конечно, наплодить чудо-хидеров, которые отчасти будут использовать ту же идеологию, но:
* чудо-хидер повышает время сборки проекта
* чудо-хидер не гарантирует, что моя IDE осознает его чудо-структуру и так удобно, как я описал, мне все подскажет
* переносимость всего этого чудо-хозяйства между компилерами неочевидна (битовые поля компилеры поддерживают, мягко говоря, по разному).
В реальности регистры устройства состоят из многих полей, и, прежде чем проициализировать регистр я должен "синтезировать" все слово, записываемое в регистр.
Т.е. в тексте у меня это будет выглядеть как-то так
Код
Обращение к кодогенератору (Ethernet.Control.Satate_Of_Chip_WR,
PKT_LEN =1560,
SPEED=100,
FLOW_CONTROL=ON)
PKT_LEN =1560,
SPEED=100,
FLOW_CONTROL=ON)
что раскроется в выражение типа
Код
*((volatile unsigned int*)0xFFFF0000)=((0x618<<24)|(1<<2)|1));
Получается все сбалансированно - описание для кодогенератора совместимо с любым программером

Ну и в качестве вершины такого подхода можно предложить обобщенные методы.
Например, у нас есть Ethernet_Init, которому в качестве параметров передается длина пакета, скорость, ну и много чего. А этот метод раскрывается в целый кусок кода:
* сборка значений в регистры
* запись в нужной поледовательности
* проверка готовности
* пр.
Тогда этот самый Ethernet контроллер для внешнего мира выглядит как компактный набор методов, которые в реальности суть очень эффективный С код.
Комбинация двух описанных выше подходов возволяет полностью исключить ручную низкоуровневую модификацию исходников при переходе от навороченного синтетического порта к, например, ATmega88P, которая еще очень долго будет "уделывать" по потреблению все эти супернавороченные ARM'ы при работе от кварца 32768.