Малые замечания.
Полная версия раздела задается в виде: версия.подверсия.выпуск.
Номер версии увеличивается при появлении глав, затрагивающих не рассмотренные ранее вопросы. Названия этих глав выносятся в оглавление раздела. Подверсия увеличивается, когда в существующие главы добавляются новые абзацы, рисунки, либо значительно перерабатывается имеющийся материал, существенно изменяя содержание главы. А при внесении лишь незначительных правок в существующие главы увеличивается номер выпуска.
Версия 1.1.0 . Добавлен пример в главе счетчики .
Характеристика реального времени.
Динамические свойства программ реального времени принято характеризовать тремя определениями: программы жесткого, мягкого и интерактивного реального времени.
Жесткое реальное время. Предусматривает наличие гарантированного времени отклика системы на конкретное событие, например, аппаратное прерывание, выдачу команды управления и т.п. Абсолютная величина времени отклика большого значения не имеет. Так, если необходимо, чтобы программа отработала некоторую команду за 1 миллисекунду, но она справляется с этим заданием лишь в 95% случаев, а в 5% не укладывается в норматив, такую систему нельзя охарактеризовать как работающую в жестком реальном времени. Если же команду нужно отработать в течение часа, что и происходит в 100% случаев – налицо жесткое реальное время.
Мягкое реальное время. В этом случае ожидающееся время отклика системы является величиной скорее индикативной, нежели директивной. Конечно, предполагается что в большинстве случаев (процентов 80 — 90) отклик уложится в заданные пределы. Однако и остальные варианты – в том числе полное отсутствие реакции системы – не должны приводить к плачевным результатам. Обычно считается, что если временной норматив превышен на один порядок, то это еще терпимо .
Интерактивное реальное время. Является скорее психологической, нежели технической характеристикой. Определяет время, в течение которого оператор – Человек – способен в безмятежности и спокойствии ожидать реакции системы на данные им указания.
Счетчики.
При использовании счетчиков всегда проверяйте граничные условия (достижение предела счета).
typedef unsigned short unsigned16; #define TIMEXPIRED 21 unsigned16 counter; void timer_function(void) if (counter > 0) counter--; > void monitor(void) counter = TIMEXPIRED; while (TRUE) < if (counter == 0) < counter = TIMEXPIRED; time_expired(); >do_something_1(); do_something_2(); // . do_something_n(); > >
В этом примере некоторая функция time_expired() должна вызываться периодически по истечении TIMEXPIRED тактов таймера. Причем вызов не может производиться непосредственно из функции обработки таймерного сигнала timer_function() , а осуществляется в мониторе monitor() . Если в обработчике таймера счет не будет останавливаться по достижении нулевого предела, возможно неконтролируемое переполнение счетчика counter . Ведь общая программная нагрузка на систему вполне может оказаться такой, что в мониторе не произойдет захвата нулевого значения и переустанова счетчика. Тогда придется долго (65535 тактов для unsigned16 счетчика) ждать очередного вызова time_expired() .
В качестве счетчиков, допускающих асинхронное управление, целесообразно использовать переменные со знаком:
typedef int int32; int32 counter; void counter_function(void) // сf_0 < if (counter >0) < // сf_2 counter—; // сf_3 if (counter == 0) < // сf_4 do_something(); // сf_5 > > > void stop_counter(void)
Периодически вызываемая функция counter_function() обслуживает декрементный счетчик counter , по истечении которого должна быть выполнена некоторая подпрограмма [cf_5]. А функция stop_counter() , которая останавливает счетчик, может вызываться асинхронно относительно counter_function() , например из аппаратного прерывания. Если в качестве counter использовать беззнаковую переменную, останов счета возможен лишь при задании нулевого значения счетчика. Это может привести к нежелательным эффектам. Если функция останова счета будет вызвана после выполнения сравнения [cf_2], но до декремента счетчика [cf_3], значение переменной counter станет максимальным положительным числом соответствующего беззнакового формата. Таким образом, счетчик не будет отключен и по истечении соответствующего числа вызовов counter_function() однократно активируется функция do_something() . Когда же останов беззнакового счетчика осуществляется после выполнения декремента [cf_3], условие [cf_4] станет истинным, даже если значение счетчика было заметно отличным от нуля. Такое поведение является не вполне ожидаемым, поскольку после останова счетчика подпрограмма do_something() — если она еще не выполняется — запускаться не должна. Переменная со знаком позволяет использовать отрицательную область значений для управления счетчиком, что дает возможность избежать отмеченных неприятностей.
Кольцевой буфер (FIFO).
Рассмотрим базовый алгоритм организации кольцевого буфера.
#define FIFO_SIZE 111 // Размер кольцевого буфера (FIFO) #define ERROR -1 // Код ошибки #define OK 1 // Код нормального завершения typedef char int8; typedef unsigned char unsigned8; typedef short int16; typedef unsigned short unsigned16; typedef int int32; typedef unsigned int unsigned32; typedef struct unsigned8 data_1[16]; int16 data_2; unsigned32 data_3; > fifodata; // Структура данных FIFO fifodata fdata[FIFO_SIZE]; // FIFO: массив структур данных unsigned16 tail=0, head=0; // Голова и хвост кольцевого буфера: определение и инициализация int16 read_fifo(fifodata *fd) // rf_0 unsigned16 t; // rf_2 if (tail == head) return ERROR; // rf_4 t = tail+1; // rf_5 if (t == FIFO_SIZE) t = 0; // rf_6 *fd = fdata[t]; // rf_7 tail = t; // rf_8 return OK; >
Функция чтения кольцевого буфера read_fifo(fd) . Проверяет наличие записей в FIFO и возвращает код ошибки ERROR для пустого буфера [rf_4]. Если в буфере имеются не прочитанные записи, самая старая из них извлекается их хвоста FIFO, заносится в параметр fd и функция завершается с кодом OK . При этом манипуляции c хвостом буфера — закольцовывание [rf_6] и вывод очередного значения [rf_7] — осуществляются в локальной переменной [rf_2]. Именно такая локализация обеспечивает взаимную независимость функций чтения и записи кольцевого буфера. Новое значение адреса хвоста tail устанавливается [rf_8] лишь после фактического освобождения соответствующего элемента FIFO [rf_7]. Собственно функция чтения буфера не является безусловно сигналобезопасной, поскольку содержит разделяемый ресурс tail . Поэтому допустимы лишь транзакционные вызовы read_fifo(fd) (см. Критические ресурсы).
int16 write_fifo(fifodata *fd) // wf_0 unsigned16 h; // wf_2 h = head+1; // wf_4 if (h == FIFO_SIZE) h = 0; // wf_5 if (h == tail) return ERROR; // wf_6 fdata[h] = *fd; // wf_7 head = h; // wf_8 return OK; >
Функция записи кольцевого буфера write_fifo(fd) . Проверяет наличие свободного места в FIFO и возвращает код ошибки ERROR когда буфер полон [wf_6]. Если в буфере есть место, в его голову заносится параметр fd и функция завершается с кодом OK . При этом манипуляции c головой буфера — закольцовывание [wf_5] и запись очередного значения [wf_7] – осуществляются в локальной переменной [wf_2]. Именно такая локализация обеспечивает взаимную независимость функций записи и чтения кольцевого буфера. Новое значение адреса головы head устанавливается [wf_8] лишь после фактического занесения данных в буфер [wf_7]. Собственно функция записи в буфер не является безусловно сигналобезопасной, поскольку содержит разделяемый ресурс head . Поэтому допустимы лишь транзакционные вызовы write_fifo(fd) (см. Критические ресурсы). Например, если готовность некоторых данных сигнализируется аппаратным прерыванием, следует запрещать повторные прерывания от того же источника на время занесения данных в кольцевой буфер.
unsigned16 nof_fifobusy(void) // nf_0 int32 ht; ht = head - tail; // nf_4 if (ht < 0) ht += FIFO_SIZE; // nf_5 return ht; >
Функция nof_fifobusy() подсчитывает число занятых элементов в кольцевом буфере. Для обеспечения сигналобезопасности относительно записи и чтения FIFO разность головы и хвоста буфера присваивается локальной переменной со знаком [nf_4]. Если же использовать в условном операторе [nf_5] переменные head и tail непосредственно, можно получить совершенно неверный результат, когда запись или чтение буфера происходят после проверки условия в [nf_5] и совпадают с закольцовыванием FIFO.
При выборе размера буфера FIFO_SIZE следует учитывать, что максимальное число хранимых данных равно FIFO_SIZE-1 . Отметим также, что мы сознательно не используем чуть более эффективный алгоритм закольцовывания FIFO: h=(head+1)&FIFO_ZIZE . Он предполагает, что размер буфера задается битовой маской: 0x1F, 0x7F и подобными значениями. Однако, если при внесении изменений в программу забыть об этом факте, FIFO потеряет работоспособность. См.также против микро-оптимизации.
Полное чтение данных.
Одной из рутинных задач приложений реального времени является асинхронный прием, буферизация и последующая обработка данных. Приведем пример, решающей эти задачи в условиях, когда факт появления новых данных сопровождается аппаратным прерыванием или сигналом.
#define FIFO_SIZE 10 // Размер кольцевого буфера (FIFO) #define ERROR -1 // Код ошибки #define OK 1 // Код нормального завершения typedef char int8; typedef unsigned char unsigned8; typedef short int16; typedef unsigned short unsigned16; typedef int int32; typedef unsigned int unsigned32; typedef struct unsigned8 data_1[8]; int16 data_2; unsigned32 data_3; > appldata; // Структура данных приложения appldata apdata[FIFO_SIZE]; // FIFO: массив структур данных unsigned16 tail, head; // Голова и хвост кольцевого буфера int16 sem_sigio, flag_sigio; // Семафор доступа к приложению и флаг новых данных int16 read_fifo(appldata *ap) unsigned16 t; if (tail == head) return ERROR; t = tail+1; if (t == FIFO_SIZE) t = 0; *ap = apdata[t]; tail = t; return OK; > int16 write_fifo(appldata *ap) unsigned16 h; h = head+1; if (h == FIFO_SIZE) h = 0; if (h == tail) return ERROR; apdata[h] = *ap; head = h; return OK; >
Функции чтения read_fifo(ap) и записи write_fifo(ap) кольцевого буфера полностью аналогичны таковым из раздела «Кольцевой буфер (FIFO)».
void process_application(appldata *ap) >
Функция приложения process_application(ap) , где производится собственно обработка данных, приведена в виде заглушки.
void read_handler(void) // rh_0 appldata ap; do < // rh_4 flag_sigio = 0; // rh_5 sem_sigio++; // rh_6 if (sem_sigio == 0) < // rh_7 while (read_fifo(&ap) == OK) < // rh_8 process_application(&ap); // rh_9 >> sem_sigio--; // rh_12 > while (flag_sigio != 0); // rh_13 flag_sigio = 1; // rh_14 >
Повторно-входимая функция read_handler() обеспечивает сигналобезопасное извлечение данных из кольцевого буфера [rh_8] с последующей их отправкой на обработку в приложение [rh_9]. Сигналобезопасность поддерживается семафором sem_sigio : закрытие [rh_6], проверка занятости [rh_7] и открытие [rh_12]. Сам по себе цикл извлечения данных из FIFO [rh_8] не может гарантировать полное считывание всех поступивших в буфер посылок. Так, если запись новых данных в FIFO произойдет после вызова функции read_fifo(ap) с результатом ERROR (нет данных в буфере), но до открытия семафора [rh_12], последние данные не будут отправлены приложению. Возможность их обработки предоставится лишь после приема хотя бы одной новой посылки. Для извлечения всех поступивших в кольцевой буфер данных и служит внешний цикл на основе флага новых данных flag_sigio . Этот флаг устанавливается [rh_14] после любого обращения к функции read_handler() , обеспечивая внешнее зацикливание [rh_4] критической секции извлечения данных из FIFO (от [rh_6] до [rh_12]). Таким образом, в случае прихода новых данных до открытия семафора [rh_12] сработает цикл [rh_13], обеспечивая дочитывание данных из буфера. Если же запись данных произойдет после открытия семафора [rh_12], но до проверки условия [rh_13], эти данные будут немедленно отправлены приложению, а затем выполнен один “холостой” внешний цикл [rh_4].
void enable_interrupt(void) > void disable_interrupt(void) >
Функции разрешения прерывания или сигнала enable_interrupt() и его запрета disable_interrupt() приведены в виде заглушек.
void interrupt_handler(void) // ih_0 appldata ap; disable_interrupt(); // ih_4 write_fifo(&ap); // ih_5 enable_interrupt(); // ih_6 read_handler(); // ih_7 >
Обработчик прерывания или сигнала. Осуществляет извлечение данных из регистров и иных источников (как правило аппаратных) и записывает эти данные в кольцевой буфер [ih_5]. На время записи повторные прерывания запрещаются [ih_4] и разрешаются сразу после занесения данных в FIFO [ih_6]. Затем осуществляется вызов сигналобезопасной функции чтения данных из буфера с последующей их обработкой в приложении [ih_7]. Отметим, что во время обработки данных в функции read_handler() прерывания или сигналы могут продолжать поступать. Новые данные при этом будут заноситься в FIFO. Таким образом, повторно входимый вызов interrupt_handler() является штатной ситуацией.
void init_io(void) tail = 0; head = 0; sem_sigio = -1; >
Функция инициализации задает начальные значения головы и хвоста кольцевого буфера и открывает семафор выполнения приложения. Флаг новых данных flag_sigio инициализации не требует, поскольку всегда сбрасывается до его использования [rh _5].
Против микро-оптимизации.
Перепишем функцию занятия ресурса в слегка “оптимизированном” виде, заменив семафорную инкрементную операцию на пре-инкрементный оператор, внедренный в проверку условия занятости ресурса (см. Семафоры ):
void semaphore_example(void) if (++sem == 0) . > else . > sem--; >
Видно, что программный код потерял ясную симметричность и способен порождать дополнительные ошибки. Так, может быть легко допущена опечатка с заменой пре-инкрементной операции на пост-инкрементную. Это не приведет к видимым, немедленно заметным последствиям, но способно вызвать серьезные нарушения в работе с критическим ресурсом. А взаимная блокировка ресурсов требует как правило выполнения лишь семафорных операций без реальной проверки их занятости. Таким образом, данная микро-оптимизация, опирающаяся на особенности инкрементных и декрементных операторов языка С , не оправдана.
Быть может, микро-оптимизация имела смысл для компиляторов семидесятых-восьмидесятых годов 20-го века и некоторые операторы языка C унаследовали такую возможность. Современные компиляторы, в том числе использующиеся в средах разработки ПО микроконтроллеров, достаточно хорошо оптимизируют код исходного языка. Поэтому существенно более значимой становится тщательная проработка прикладных алгоритмов, которые могут быть достаточно сложными и изощренными. Целесообразно также минимизировать использование особо специфических операторов и трюков языка программирования.
Ясность, прозрачность, надежность исходного кода – не в ущерб его продвинутости, хотя, возможно, с незначительной потерей эффективности – являются ключевыми чертами по-настоящему профессионально разработанных программ. А для “скрытого” программного обеспечения, эти черты становятся также и настоятельной необходимостью.
Буфер FIFO
Преобразователь параллельного цифрового кода в последовательный код построен на базе микросхемы Hitachi MBF1250. Микросхема Hitachi MBF1250 — это буфер типа FIFO(First Input First Output) с размером матрицы 128Кx8 и, по сути дела представляет собой последовательно соединённые параллельные восьмиразрядные регистры сдвига. Микросхема работает в двух режимах: когда на входе управления появляется сигнал высокого уровня, он запускает первый генератор, систему управления АЦП, при этом происходит запись восьмиразрядных кодов из АЦП в буфер; во втором режиме, когда сигнал управления сообщает об отсутствии информации на входе АЦП, происходит остановка работы первого генератора и самого АЦП, при этом запускается второй генератор, синхронизирующий работу буфера, производящего выгрузку данных. Назначение выводов:
Рис.4. Буфер Hitachi MBF1250
3. Запись WR Вход D4
4. Тактовый вход C Вход D5
7. Разрешение на чтение
8. Прямой выход Q7
19. Задержка такта DE
20. Последовательный вход D
Генераторы тактовых импульсов
Рис.5. ГТИ
ГТИ мы построили на базе ИС КР531ГГ1, которая представляет собой два независимых генератора. Если на вход U подать высокий уровень, а на DU низкий, то для фиксации частоты потребуется подсоединить между входами Свн внешний элемент-конденсатор. На выходах мультивибраторов получается меандр с частотой:
Если на вход EI подать напряжение высокого уровня, то это запретит работу генератора. Один генератор мы будем использовать для работы АЦП и загрузке буфера, а другой при разгрузке буфера. Причём G1 работает с частотой 1МГц>Свн1=0,0005/1МГц=500пФ, а G2 работает с частотой 200кГц>Свн2=0,0005/200кГц=2500пФ.
Счетчик импульсов
Используем микросхему К555ИЕ9 (DD7) четырехразрядный двоично-десятичный счетчик с асинхронным сбросом, дешифрующим счетным выходом, с возможностью асинхронной установки в произвольное состояние от нуля до девяти.
Данный счетчик является составной частью системы управления АЦП и преобразователя параллельного кода в последовательный. Его задача состоит в счете от 0 до 9, преобразование последовательности тактовых импульсов в параллельный четырехразрядный код, для последующего преобразования его ТТЛ логикой в сигналы управления.
Тактовые импульсы подаются с генератора на вход С. Он работает по переднему фронту входного импульса (01). Так как счетчик работает постоянно, то нас не интересует какое значение установится при его запуске, т.е. предварительный сброс счетчика в ноль не требуется, поэтому на вход R подадим потенциал высокого уровня.
Предварительная запись значения в счетчик по входам D1, D2, D3, D4 нас не интересует поэтому необходимо эти выводы микросхемы заземлить. Так как нет предварительной записи, то не требуется и вход разрешающий предварительную запись V2. На этот вывод подадим потенциал высокого уровня.
Вывод P2 выдает высокий уровень напряжения через каждые десять тактов, когда значение в счетчике равно девяти (Q1 = Q4 = 1 ; Q2 = Q3 = 0). В нашей схеме мы его не используем.
Вывод P1 используется для разрешения переноса импульса в следующий каскад (если соединяются несколько счетчиков последовательно). У нас только один счетчик поэтому на вывод P1 должно постоянно подаваться напряжение высокого уровня. На вход разрешения счета V1, в зависимости от режима работы АЦП, подаётся напряжение с сигнала управления. Высокий уровень разрешает работу счетчика, низкий блокирует. Выход R также подсоединяем к СУ. Низкий уровень обнулирует счетчик.
Выводы P1, V2 — подаем высокий уровень напряжения
Выводы D1, D2, D3, D4 — заземляем
Назначение выводов ИС К555ИЕ9
1. Вход “установка L” R
2. Вход синхронизации С
3. Вход информационный D1
4. Вход информационный D2
5. Вход информационный D3
6. Вход информационный D4
7. Вход разрешения счета V1
9. Вход разрешения предварительной записи V2
10. Вход разрешения переноса P1
11. Выход четвертого разряда Q4
12. Выход третьего разряда Q3
13. Выход второго разряда Q2
14. Выход первого разряда Q1
15. Выход переноса
Рис.6. ИС К555ИЕ9
Микросхема К555ИЕ9 имеет следующие характеристики
( Ucc = 5,25 В; U 1 вых 2,7 В; U 0 вых 0,5 В; Iпотр 31 мА; I 0 вх -0,4 мА; I 1 вх 0,02 мА;
I 0 вых 8 мА; I 1 вых -0,4 мА; tздр 39 нс )
Потребляемая мощность микросхемы К555ИЕ9 равна:
Особенности FIFO буфера UART в ESP32
У ESP32 есть три UART-а. Каждый из которых размещает FIFO буфер приемника и FIFO буфер передатчика в общей памяти размером 1024 байта (ESP32 technical reference manual 3.5):
Однако при попытке увеличить размер FIFO буфера передатчика UART2 с 128 до 256 байт получил неожиданный эффект — передаваемые данные портили FIFO буфер приемника UART0, чего согласно документации быть не должно.
К сожалению в документации (ESP32 technical reference manual 3.5) описание регистров FIFO буферов отсутствует. Однако в заголовочных файлах esp-idf (в uart_struct.h) обнаружились:
1) регистр состояния FIFO буфера передатчика (смещение относительно базового адреса 0x5c):
union < struct < uint32_t status:24; uint32_t reserved24: 8; >; uint32_t val; > mem_tx_status;
2) регистр состояния FIFO буфера приемника (смещение относительно базового адреса 0x60):
union < struct < uint32_t status: 24; uint32_t reserved24: 8; >; struct < uint32_t reserved0: 2; uint32_t rd_addr: 11; /*This register stores the rx mem read address.*/ uint32_t wr_addr: 11; /*This register stores the rx mem write address.*/ uint32_t reserved: 8; >; uint32_t val; > mem_rx_status;
Полагая что mem_tx_status соответствует по назначению бит mem_rx_status пишем для получения адресов FIFO буферов следующий код:
#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include void print_uart_st(uart_dev_t *u,int num) < printf("UART%d:\n",num); printf("rx_st=0x%X\n",(unsigned int)u->mem_rx_status.val); printf("rx_rd=0x%X\n",(unsigned int)u->mem_rx_status.rd_addr); printf("rx_wr=0x%X\n",(unsigned int)u->mem_rx_status.wr_addr); uint32_t tx_s = u->mem_tx_status.val; printf("tx_st=0x%X\n",tx_s); printf("tx_rd=0x%X\n",(tx_s>>2)&2047); printf("tx_wr=0x%X\n",(tx_s>>13)&2047); > uart_config_t uart_config = < .baud_rate = 115200, .data_bits = UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE >; void UARTtest(void * param) < uart_param_config(UART_NUM_1,&uart_config); uart_param_config(UART_NUM_2,&uart_config); uart_driver_install(UART_NUM_1, 256, 0, 0, NULL, 0); uart_driver_install(UART_NUM_2, 256, 0, 0, NULL, 0); uart_set_pin(UART_NUM_1, UART_PIN_NO_CHANGE, GPIO_NUM_16, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); uart_set_pin(UART_NUM_2, UART_PIN_NO_CHANGE, GPIO_NUM_16, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); vTaskDelay(1000/portTICK_PERIOD_MS); while (1) < print_uart_st(&UART0,0); print_uart_st(&UART1,1); print_uart_st(&UART2,2); vTaskDelay(2000/portTICK_PERIOD_MS); char s[256]; uart_write_bytes(UART_NUM_1, s, 1); uart_write_bytes(UART_NUM_2, s, 2); if(gets(s)!=NULL) < printf("recived=%s\n",s); >> > void app_main(void)
После запуска получаем:
UART0:
rx_st=0x300600
rx_rd=0x180
rx_wr=0x180
tx_st=0xCE058
tx_rd=0x16
tx_wr=0x67
UART1:
rx_st=0x400800
rx_rd=0x200
rx_wr=0x200
tx_st=0x100200
tx_rd=0x80
tx_wr=0x80
UART2:
rx_st=0x500A00
rx_rd=0x280
rx_wr=0x280
tx_st=0x200400
tx_rd=0x100
tx_wr=0x100
Вывод осуществляется через UART0, поэтому tx_wr и tx_rd отличны от 0. Согласно полученным результатам распределение памяти между FIFO буферами UART0,1,2 следующее:
Адреса | UART |
---|---|
0x00. 0x7F | UART0 TX_FIFO |
0x80. 0xFF | UART1 TX_FIFO |
0x100. 0x17F | UART2 TX_FIFO |
0x180. 0x1FF | UART0 RX_FIFO |
0x200. 0x27F | UART1 RX_FIFO |
0x280. 0x2FF | UART2 RX_FIFO |
Кроме того регистры состояния FIFO буферов имеют разрядность 11 бит, значит возможно размер памяти отводимой под FIFO буфера = 2Кб. При задании UART0.mem_conf.tx_size=15 (память выделяется кусками длинной 128 байт) будет выделено 1920 байт, и регистр tx_wr при передаче досчитает до 1919 и с последующим переходом через 0 продолжит счет. Однако память при этом адресуется только мл. 10 битами, т.е. реальный объем общей памяти отведенной под FIFO буфера = 1Кб.
- Распределение общей памяти, отводимой ESP32 под FIFO UART не соответствует приведенной в technical reference manual 3.5;
- Объем общей памяти FIFO UART = 1Кб.
Базовые принципы построения FIFO.
Очередь FIFO (First In, First Out) представляет собой циклический буфер, где будут храниться помещаемые в очередь данные. Есть два указателя: указатель на «голову» очереди ( head ) и указатель на «хвост» очереди ( tail ).
Новый записываемый элемент помещается в ячейку, на которую указывает head , затем этот указатель перемещается на следующую ячейку памяти буфера.
Выбираются элементы из очереди по указателю tail , после того, как элемент выбран из очереди, указатель tail так же передвигается вперед к следующей ячейке.
Обычно глубина FIFO и разрядность указателей являются числами степени двойки. Например, если разрядность указателей head и tail – это 8, то глубина FIFO будет 256 элементов.
Цикличность буфера обеспечивается автоматически. При последовательном увеличении значения указателя однажды возникнает перенос, а сам указатель при этом обнуляется (после указателя 255 его следующее значение будет 0).
Теперь, когда мы знаем, что есть два указателя, легко определить всякие информационные сигналы:
- FIFO пусто, если значения указателей «голова» и «хвост» равны.
- Число элементов хранимых в FIFO можно вычислить отняв от значения «головы» значение указателя «хвоста». Если получится отрицательное число, то нужно еще прибавить глубину FIFO.
- FIFO полное, если число хранимых элементов на единицу меньше, чем глубина FIFO.
Возможно, последнее утверждение нуждается в пояснении. Например, глубина FIFO 256 элементов. Мы запишем в него 255 элементов, но читать из FIFO пока не будем. Тогда «голова»=255, а «хвост»=0. Сейчас FIFO полное. Если в него записать еще один элемент, то значение «головы» станет 0 (возникает перенос) и получится, что и «голова» и «хвост» становятся одинаковыми и равными нулю. Все, FIFO опять стало пустым? Конечно, это ошибка логики.
Нельзя допускать запись в полное FIFO и чтение/выборку элемента из пустого FIFO.
Все вышеописанное прекрасно, но что делать, если запись и чтение происходят с использованием разных тактовых частот, если FIFO асинхронное? Мы не можем на прямую сравнивать или делать какие-то арифметические операции с указателями «голова» и «хвост», так как в этом случае они храняться в разных клоковых доменах (clock domains).
Просто пересинхронизировать группу (8-ми битные счетчики указателей голова и хвост) сигналов в другой домен не есть хороший вариант, как было уже ранее показано в другой статье. Главная проблема здесь – не все биты шины могут перейти в другой клоковый домен одновременно, а значит двоичное число на шине может быть искажено.
С другой стороны, понятно, что и «хвост» и «голова» могут изменяться только на единицу за один раз. То есть они считают последовательно. Этот факт как раз и делает возможным использование специальных счетчиков — счетчиков Грея.
Код Грея — специальная система счисления, в которой два соседних значения различаются только в одном разряде.
Вот пример трехбитных кодов Грея. Слева в таблице значения обычного двоичного счетчика, а справа значения из счетчика Грея.
Методика использования кодов Грея может быть следующая. Значение счетчика указателя, например, головы FIFO перекодируется в код Грея и пересекает клоковый домен с помощью группы синхронизаторов (каждый – это два последовательных триггера). При этом, как мы знаем, не все биты могут быть зафиксированы верно: в новом клоковом домене в некоторых битах зафиксируются новые значения числа на шине, а в некоторых – старые значения. Однако, для кода Грея изменения в соседних числах счетчика происходят только в одном бите, значит только в одном бите и может произойти коллизия. Только один изменяющийся сейчас бит может быть принят не верно. В этом случае принимающий домен просто считает предыдущее число на шине, а верное, следующее число он считает на следующем такте своей частоты. Такая ошибка даже и ошибкой не считается, ведь она не приводит к аварии FIFO.
Принятый код Грея может быть перекодирован обратно в обычную двоичную систему счисления и тут уже мы имеем верное значение обоих указателей в одном клоковом домене. Теперь можно их сравнивать, вычислять число элементов в FIFO и так далее.
В принципе, FIFO может изначально держать указатели «головы» и «хвоста» в кодах Грея.
Вообще использование кодов Грея в асинхронных FIFO очень распространено, так как действительно позволяет решить проблему пересечения клокового домена для потока данных.