Pull to refresh

Работа с регистрами внешних устройств в языке C, часть 3

Reading time6 min
Views18K
Все хорошо, что хорошо кончается

Теперь, когда мы рассмотрели, как с помощью средств языка С мы сможем определить фиксированное расположение регистра в адресном пространстве МК (часть 1), как мы сможем определить отдельные битовые группы в регистре (часть 2), самое время рассмотреть как мы можем с этими группами работать. Работа с группой битов, как с целым, не представляет никаких проблем, опирается на их описание в виде битовых полей и уже демонстрировалась, однако нам может потребоваться и работа с отдельными битами поля, причем по соображениям эффективности либо понятности программы разделять группу на отдельные поля нецелесообразно.
Допустим, что нам необходимо отдельно манипулировать старшим битом поля команды из нашего примера. Первое, что приходит в голову, это union, однако объединения не могут иметь битовую длину. Есть вариант создать две версии описания регистров и уже их объединить, и он работает:
#pragma bitfields=reversed
typedef struct {
  unsigned :1;
  unsigned int code:3;
  unsigned :26;
  const unsigned flag1:1;
  unsigned flag:1;
} tIO_STATUS;
typedef struct {
  unsigned :1;
  unsigned int start:1;
  unsigned :30;
} tIO_STATUSA;
#pragma bitfields=default 
typedef union {
  tIO_STATUS;
  tIO_STATUSA;
} tIO_STATUS2;  
#define IO_ADR 0x20000004
volatile tIO_STATUS2 * const pio_device = (tIO_STATUS2 *) (IO_ADR);
  pio_device->code = 3;
  while (pio_device->flag) {};
  pio_device->start=1;
, но создание двух дополнительных типов несколько избыточно (на мой взгляд).
Альтернативой для манипуляций отдельным битами группы являются все те же битовые маски и мы приходим к кострукциям типа:
#define BITNUM 2 // биты в группе нумеруются с 0
#define BITMASK (1<<BITNUM)
  pio_device->code |= (1<< BITNUM); // устанавливаем бит
  pio_device->code &= ~BITMASK;; // сбрасываем бит
Обратим внимание на то, что контроль допустимости значения для подобных операций (в отличие от присвоения константы) компилятор не проводит. Также заметим, что применен номер бита, из которого путем сдвига создается битовая маска. Я так делаю, поскольку в наборе номера ошибиться сложнее, нежели в наборе битовой маски (0х40000000), причем тут еще надо в уме считать, а разницы в коде нет никакой (но это, конечно, дело вкуса). А вот теперь действительно серьезное замечание — все авторы статей о встроенном программировании (а я в том числе) категорически НЕ рекомендуют применять в тексте подобные конструкции, а определять макросы для установки и сброса битовых полей
#define SETBIT(DEST,MASK) (DEST) |= (MASK)
#define CLRBIT(DEST,MASK) (DEST) &= ~(MASK)
и дальше использовать только их.
 SETBIT(pio_device->code,1 << BITNUM);
 CLRBIT(pio_device->code,BITMASK);
Во-первых, вы не допустите обидную ошибку, забыв во втором случае поставить побитовое отрицание (~) либо поставив вместо него логическое отрицание (!) (те, кто никогда подобной ошибки не делал, очень внимательные люди, я, к сожалению, к ним не принадлежу). Во-вторых, перейдя на МК с побитовой адресацией, вы сможете этот макрос переопределить (только для единственных битов) с учетом возможностей аппаратуры и получить существенно более быстрый код. В-третьих, если (когда) вам придется превращать эти операции в атомарные, намного проще сделать это в определении макроса, нежели гоняться за ними по всей программе.

Вот о последнем аспекте есть смысл поговорить несколько подробнее. Как известно, необходимость атомарных операций возникает при наличии более чем одного процессов, конкурирующих за доступ к какому-либо ресурсу. Так вот, для доступа к регистрам внешних устройств даже в случае одного процесса (главного цикла While) существует неявная конкуренция со стороны подрограмм обслуживания прерываний.Поэтому при обращении к регистрам ВУ последовательность чтение-модификация-запись представляет угрозу с точки зрения обеспечения непрерывности действий. Дело в том, что имеющиеся в наборе команд операции МК установки/сброса битов по маске НЕ могут оперировать с ячейками адресного пространства напрямую и, соответственно, не могут обеспечить атомарное изменение регистра ВУ. Нельзя сказать, что разработчики МК не понимают недостатков подобного подхода, но прямого решения проблемы до сих пор не существует, что очевидно свидетельствует о наличии глубоких внутренних аспектов, препятствующих таковому. Известны различные подходы к проблемме. Наличие двух регистров в адресном пространстве, запись единицы в один из них устанавливает бит значения, запись единицы в другой сбрасывает бит значения. Наличие механизма bit-banding, когда каждому биту соответствует отдельное значение в пространстве адресов (естественно, помимо обычного механизма доступа ко всему регистру в целом). Ну и широко распространенный механизм отключения перываний перед началом операции с разрешением по ее окончании. Может быть я не в курсе, но для МК до сих пор нет хороших аппаратных атомарных операций.

Теперь поговорим о константах. Как правило, для общения с регистрами ВУ существует определенный набор допустимых значений полей и хорошим стилем программирования следует считать описание этих возможностей в виде набора констант с осмысленными именами и проверка на допустимость значения при присвоении (пока что я использовал магическое число 3, но это исключительно в учебных целях). Какие возможности предоставляет нам язык С для решения данной задачи? Их две — определение констант через #define и создание перечислимых типов. Разберем каждую из этих альтернатив. Предположим, что наше устройство способно принимать только 2 команды — «начать работу» с кодом 3 и «прекратить работу» с кодом 2. Тогда мы можем написать:
#define IO_DEVICE_START 3
#define IO_DEVICE_STOP 2
 pio_device->code=IO_DEVICE_START;
, что чаще всего и делается. Итак магическое число исчезло, даже проверка есть на соответствие размеру битового поля, но выражение
 pio_device->code=1;
компилятором будет пропущено, как допустимое. То есть задача контроля значения на допустимость ложится на плечи разработчика и реализуется ASSERTом. Метод вполне работоспособен, часто применяем и вполне приемлем, если бы не было более удобного, а именно применение перечислимого типа:
#pragma bitfields=reversed
typedef struct {
  unsigned :1;
  enum {
    O_DEVICE_START=3,
    IO_DEVICE_STOP=2,
  }  code:3;
  unsigned :26;
  const unsigned flag1:1;
  unsigned flag:1;
} tIO_STATUS;
#pragma bitfields=default 
 pio_device->code=IO_DEVICE_START;
 SETBIT(pio_device->code,BITMASK); 
 pio_device->code |= BITMASK;
 pio_device->code=pio_device | BITMASK;
Обратим внимание на то, что в последней строке мы получим предупреждение о несовместимости типов, а в двух предыдущих, которые делают то же самое, не получим (это не баг, это фича такая). Почему же этот метод удобнее? Во-первых мы можем разместить перечисление возможных значений прямо в теле описания структуры, что более читаемо. Во-вторых, компилятор проверит значения в определениях и не позволит выйти нам за рамки размера поля. В-третьих, и это главное, компилятор не позволит нам присвоить полю недопустимое (не указаное в списке) значение, хотя оставляет нам лазейку, показанную в предпоследней строке (если кто знает, как ее закрыть, напишите). Короче, все чудесно и замечательно, НО использовать такую конструкцию вы сможете не во всяком компиляторе, поскольку стандарт С не разрешает использовать для битовых полей ничего, кроме int. Кроме того, даже в IAR потребуется дополнительная директива компилятора --enum_is_int для обеспечения правильного выравнивания. Но если Вас не пугает компиляторозависимость, то метод очень красивый, прозрачный и удобный (заранее соглашаюсь с теми, кто напишет в коментах, что это сильно сократит возможности портирования).

Ну и в заключение несколько мыслей по поводу функций и оберток к ним. Часто при просмотре бибилиотек вы встретите что то похожее на следующее:
dev_data_r_w (int n, int data_command, int r_w, int *adr) { ... };
int dev_data(int n, int data_comand, iint *adr) { return dev_data_r_w (n, 1, 1, int *adr);
int read_dev(int n, int *adr) { return dev_data(n,1,adr); };
int ch_read_dev( int *adr) { return read_dev(1,adr); };
, причем нетрудно видеть, что настоящую работу делает первая функция, а все остальные создают обертки для нее, чтобы не писать соответствующие константные параметры. В языке С++ (и в ряде других) подобная проблема снята значениями параметров по умолчанию, но для С все еще актуальна. Мое личное мнение — так делать не следует. Если не требуется динамическое преобразование типов, то для создания удобных (простых в обращении) синонимов общей функции используйте макросы:
#define dev_data(N,DC,ADR) dev_data_r_w ((N),(DC),1,(ADR))
#define read_dev(N,ADR) dev_data((N),1,(ADR))
#define ch_read_dev(ADR) read_dev(1,(ADR))
Такое определение не сложнее, слегка проигрывает по размеру кода, но выигрывает по времени исполнения и размеру используемой памяти. Особенно умиляют подобные многозвенные конструкции в подпрограммах обработки прерываний. И еще одно наблюдение — некоторые программисты почему-то (если среди читателей будут такие, напишите почему) считают, что создание своего перечислимого типа
enum { SET=1, RESET=0 } ACTIVE;
— это круто. Я еще могу понять, когда такой тип используется для записи значения в бит, но когда для контроля его значения? Мне кажется, что тип bool вполне такой тип заменяет, хотя кто знает, готов выслушать и иные мнения.

Подвожу итог третьей части статьи — целью было договорится о некоторых общих правилах в описании методов доступа к регистрам ВУ, выработать некоторый словарь, поскольку я думаю о написании нескольких постов, в которых шаг за шагом разобрать построение подпрограмм обслуживания периферийных устройств МК от самых простых (SPI,UART) (хотя при глубоком рассмотрении совсем простых устройств не так и много) до достаточно сложных (USB,Ethernet). В принципе задача выполнена, осталось еще ряд замечаний по оформлению программ, но их буду излагать уже по ходу дела.
Tags:
Hubs:
+11
Comments4

Articles