Хабрахабр

Мой «Hello World!» на FPGA или очередная версия UART

Мой Hello World! на FPGA или очередная версия UART

Наконец-то у меня дошли руки до изучения ПЛИС. А то как-то неправильно получается: драйвера на железо под Linux пишу, микроконтроллеры программирую, схемы читаю (и немного проектирую), надо расти дальше.

А именно написать модули приемника и передатчика для UART, объединить их внутри FPGA (заодно понять как использовать IP Core), ну и протестировать это все на реальном железе.
Сразу говорю, что сделать универсальное параметризированное ядро задачи не стояло. Так как мигать светодиодами мне показалось не интересно, то решил сделать простенькую вещь. Это просто тестовый проект, на предмет «пощупать, что такое FPGA и как с ней общаться».

Алгоритм достаточно хорошо описан, поэтому повторю здесь только основные его моменты. Итак, начнем с приемника.

  • Частоту семплирования сигнала RX выберем в четыре раза больше, чем требуемая скорость передачи по UART.
  • Условием начала приема будем считать переход входного сигнала RX из высокого уровня в низкий в случае, если в данный момент прием еще не ведется.
  • Условием достоверного опознания стартового бита будем считать удержание сигнала RX в состоянии низкого уровня на втором такте частоты семплирования. При этом мы практически попадаем на середину битового импульса, что позволит в дальнейшем производить семплирование импульсов через каждые 4 такта.
  • В случае ошибки в стартовом либо в стоповом битах устанавливаем сигнал ошибки error. На его основе формируем сигнал fastsync, который в дальнейшем будем использовать для быстрой синхронизации приемника.
  • После опознавания стартового бита начинаем последовательный прием битов данных, начиная с самого младшего. Принимаемые данные записываем в регистр со сдвигом вправо на один бит. Условием окончания приема будет являться обнаружение стартового бита в 0-й позиции сдвигового регистра.
  • Быстрая синхронизация приемника заключается в приведении его в исходное состояние после обнаружения ошибки при переходе сигнала RX в высокий уровень (это может быть как передача логической «1», так и передача стоп-бита либо состояние бездействия линии передачи).
  • Условием успешного завершения приема (корректных значений стартового и стопового битов) является сигнал complete. От него (при тактировании сигналом rdclk) формируется импульсный сигнал ready, который обозначает наличие валидных данных на шине rxdata.

Сразу отмечу, что тактировать сигнал чтения ready от тактового сигнала clk я не хотел (неожиданно, да?), чтобы не завязавать скорость последующей обработки данных на скорость обмена по UART. Аналогичная реализация и в модуле передатчика (см. ниже). А тестовая связка модулей приемника и передатчика сделана на основе IP Core FIFO от Intel, причем с возможностью имитации различных скоростей для потребителя и генератора данных.

Модуль приемника (Verilog)

//
// Блок приемника UART
//
// Данные rxdata валидны, когда ready==1 и error==0.
// Сигнал ready устанавливается в 1 после завершения приема на один такт rdclk.
//
// Реализация:
// После перехода сигнала rx в низкий уровень проверяем его неизменность в течении
// 2-х тактов. Если уровень остался низким, значит начат прием стартового бита.
// В этом случае делаем прием 8 битов данных и одного стоп-бита (всего 9 битов).
// 2 Такта - это приблизительно середина бита, так что устойчивость должна быть
// хорошей.
//
// Биты передаются начиная от младшего к старшему.
// Лог. '0' передается низким уровнем сигнала, лог. '1' передается высоким уровнем сигнала
// idle передается высоким уровнем сигнала (лог. '1')
// start-бит передается низким уровнем сигнала (лог. '0')
// stop-бит передается высоким уровнем сигнала (лог. '1')
module uart_rx( nreset, // сигнал сброса (асинхронный, активный уровень 0) clk, // тактовая частота UART, д.б. в четыре раза больше скорости обмена по UART rx, // входная линия UART rdclk, // тактирование чтения результата приема (rxdata, ready) rxdata, // принятые данные, значение валидно при ready==1 ready, // индикатор валидности данных rxdata (активный уровень 1) error, // индикатор ошибки приема (активный уровень 1) busy, // индикатор занятости модуля (идет прием, активный уровень 1) idle); // индикатор свободной линии приемника (активный уровень 1) input wire nreset; // сигнал сброса (асинхронный, активный уровень 0)
input wire clk; // тактовая частота, д.б. в четыре раза больше скорости обмена по UART
input wire rx; // входная линия UART
input wire rdclk; // тактирование чтения результата приема
output wire[7:0] rxdata;
output wire ready;
output error;
output busy;
output idle; // Изменение сигнала завершения приема, тактируемое через rdclk
reg[1:0] done = 2'b00; // Выходной сигнал готовности принятых данных, тактируемый rdclk
assign ready = (done == 2'b10) ? 1'b1 : 1'b0; // Признак наличия ошибки приема
reg error = 1'b0; // Сигнал сброса логики приемника для быстрой синхронизации при ошибке
// Если на текущем такте имеем ранее установленный сигнал error и высокий
// уровень сигнала rx, возможно это пауза между передаваемыми байтами данных.
wire fastsync = (error && rx); // Признак свободной линии приемика
reg idle = 1'b1; // Принятые данные:
// d[9] - стоповый бит, д.б. == 1
// d[8:1] - данные
// d[0] - стартовый бит, д.б. == 0
reg[9:0] d = 10'b1xxxxxxxx1; // Статус приема. Завершение приема индицируется значением 2'b10
wire[1:0] status = ; // Признак завершения приема.
wire complete = (status == 2'b10) ? 1'b1 : 1'b0; // Принятый байт данных
assign rxdata = d[8:1]; // Признак занятости модуля
reg busy = 0;
// Счетчик тактовых импульсов до семплирования линии rx
reg[1:0] cnt; always @(posedge clk, negedge nreset)
begin if(!nreset) begin rxreset(); end else begin if(fastsync) begin rxreset(); end else begin if(busy == 1'b1) begin // Идет прием чего-то, проверяем необходимость семплинга rx if(cnt == 2'd0) begin // Записываем принятый бит // в старший разряд данных со сдвигом предыдущего значения вправо // (т.к. передача идет от младшего бита к старшему) d <= { rx, d[9:1] }; if(d[1] == 1'b0) begin // На этом шаге стартовый бит попадет в последнюю позицию, прием завершен busy <= 1'b0; // Проверяем корректность стопового бита error <= (rx == 1'b1) ? 1'b0 : 1'b1; end else begin // Мы находимся в процессе приема if(rx && (d == 10'b1111111111)) begin // Слишком маленькая длительность стартового бита busy <= 1'b0; // Индицируем наличие ошибки error <= 1'b1; end else begin // Нормальная процедура приема // Кол-во тактов целого бита - подготовка к приему следующего бита cnt <= 2'd3; end end end else begin // Уменьшаем кол-во оставшихся до семплинга тактов cnt <= cnt - 2'd1; end end else begin // Модуль пока еще ничего не делает if(!error) begin // Нет сигнала ошибки, можно попытаться начать прием стартового бита if(rx == 1'b0) begin // Линия приемника в низком уровне и до этого приема не было - начинаем работу busy <= 1'b1; // Инициализируем буфер приема данных. Здесь критично записать все 1, т.к. окончание // приема определяется состоянием d[0]==0 d <= 10'b1111111111; // Проверять линию rx будем через 1/2 длительности бита // 1-й такт - это текущее сэмплирование // 2-й такт - это следующее сэмплирование (cnt будет 0) cnt <= 2'd0; // Т.к. мы потенциально начали прием, отмечаем линию как занятую idle <= 1'b0; end else begin // Линия приемника свободна idle <= 1'b1; end end end end end
end task rxreset; begin // Сброс признака ошибки error <= 1'b0; // Установка сигнала свободной линии (!?) idle <= 1'b1; // Сброс признака занятости модуля busy <= 0; // В принципе можно записать что-нибудь, лишь бы статус не попал в complete d <= 10'b1xxxxxxxx1; end
endtask always @(negedge rdclk, negedge nreset)
begin if(!nreset) begin done <= 2'b00; end else begin // По тактам чтения сохраняем состояние сигнала complete. // Логика сигнала формирования сигнала ready формирует один импульс при // изменение сигнала complete с 0 на 1 на один такт rdclk. done <= { complete, done[1] }; end
end endmodule

Так как входной сигнал RX является асинхронным и (возможно) нестабильным, в главном модуле перед модулем приемника был подключен мажоритарный элемент. Элемент также написан на Verilog, но его код здесь приводить смысла нет. Вместо него красивая картинка синтезированного элемента.

Синтезированная схема мажоритарного элемента

Мажоритарный элемент

Блок передатчика еще проще и, надеюсь, в дополнительных комментариях не нуждается.

Модуль передатчика (Verilog)

//
// Блок передатчика UART
//
// Сигналы:
// clk - частота должна быть в 4 раза больше скорости передачи, скважность не важна
// rdclk - тактирование обмена txdata, write, fetch. Частота д.б. выше clk
// txdata - данные для передачи, управляются сигналами write/fetch
// write - источник имеет данные для передачи (1=да)
// fetch - модуль принял данные для передачи (1=да)
// tx - линия передачи UART
// idle - линия передачи свободна (1=да, информационный сигнал)
//
// Для FIFO нужно использовать режим dcfifo_component.lpm_showahead = "ON"
module uart_tx( nreset, // сигнал сброса (асинхронный, активный уровень 0) clk, // тактовая частота UART, д.б. в четыре раза больше скорости обмена по UART rdclk, // тактирование подтверждения приема данных от поставщика txdata, // шина данных на передачу от поставщика write, // признак наличия данных на передачу (активный уровень 1) idle, // индикатор не активного передатчика (активный уровень 1) fetch, // подтверждение загрузки данных от поставщика, тактируется rdclk tx); // выходная линия UART input wire nreset; // сигнал сброса (асинхронный, активный уровень 0)
input wire clk; // тактирование UART
input wire rdclk;
input wire[7:0] txdata;
input wire write;
output wire idle;
output fetch;
output tx; // Состояние выходной линии
reg tx = 1'b1; reg fetch = 1'b0; // Делитель частоты на 4
reg[1:0] div4 = 2'd0; // Состояние машины:
reg[3:0] s = 4'd10; // Передатчик полностью свободен
assign idle = (s == 4'd10); // Сдвиговый регистр данных
reg[7:0] d; // Признак передачи стартового бита в данном цикле
reg sendstart; // Признак возможности запроса новых данных на передачу
reg canfetch; // Признак завершения ввода новых данных, тактируется clk
reg gotdata = 1'b0; // Для синхронизации clock domains
reg[1:0] sync = 2'b00; // Запомненный по rdclk сигнал write
reg wr = 1'b0; // При появлении запроса getdata==1 при наличии данных у внешнего источника
// производится их запоминание в регистре nextdata и устанавливается признак
// готовности gotdata==1. Кроме того, для внешнего источника данных формируется
// сигнал подтверждени.
// Сигнал gotdata снимается при снятии сигнала getdata.
always @(posedge rdclk, negedge nreset)
begin if(!nreset) begin wr <= 1'b0; sync <= 2'b00; // Сбрасываем сигнал подтверждения ввода данных fetch <= 1'b0; end else begin // Запоминаем сигнал write wr <= write; // Проверяем появление запроса новых данных для передачи sync <= { gotdata, sync[1] }; if(gotdata && (sync[1] == 1'b0)) begin // Устанавливаем признак подтверждения для источника данных fetch <= 1'b1; end else begin // Сбрасываем сигнал подтверждения приема данных fetch <= 1'b0; end end
end always @(posedge clk, negedge nreset)
begin if(!nreset) begin // Установка передатчика в исходное состояние div4 <= 2'd0; s <= 4'd10; gotdata <= 1'b0; end else begin // Пока нет признака передачи стартового бита в этом цикле sendstart = 1'b0; // Начальная установка признака запроса данных на передачу canfetch = wr; if(div4 == 2'd0) begin case(s) 4'd0: begin // Передача стартового бита будет инициирована ниже sendstart = 1'b1; // Передатчик занят, нельзя запрашивать новые данные canfetch = 1'b0; end 4'd9: begin // Передача стопового бита tx <= 1'b1; end 4'd10: begin // Состояние idle, ничего не делаем end default: begin // Идет передача битов данных, текущий младший является выходом tx <= d[0]; // Выполняем сдвиг данных вправо d <= { 1'b0, d[7:1] }; // Передатчик занят, нельзя запрашивать новые данные canfetch = 1'b0; end endcase end else begin // Выдерживаем текущее состояние div4 <= div4 - 2'd1; if(s < 4'd9) begin // При выдерживании до состояния 9 прием новых данных невозможен! canfetch = 1'b0; end end if(canfetch) begin // Входные данные готовы, передаем их на обработку d <= txdata; // Подтверждение взятия данных на обработку gotdata <= 1'b1; if(idle /*s == 4'd10*/) begin // Состояние idle - немедленно начинаем передачу стартового бита sendstart = 1'b1; end else begin // На следующем шаге переходим к передаче стартового бита s <= 4'd0; end end if(gotdata) begin // Данные были приняты ранее, снимаем сигнал подтверждения gotdata <= 1'b0; end if(sendstart) begin // На данном шаге начинаем передачу стартового бита tx <= 1'b0; // Переходим к следующему состоянию s <= 4'd1; // Длительность стартового бита div4 <= 2'd3; end else begin if(div4 == 2'd0) begin if(s < 4'd10) begin // Последовательное изменение состояния на следующее s <= s + 4'd1; // Время выдерживания состояния div4 <= 2'd3; end end end end
end endmodule

Для тестирования приемника и передатчика на коленке был написан главный модуль. На него прошу не ругаться, ошибки проектирования (внешний асинхронный сигнал nreset, отсутствие сброса FIFO и т.п.) я и сам знаю. Но для целей проверки функциональности они не существенны.

Поэтому в главном модуле я использовал PLL, на выходе C0 которого сформировал частоту для работы c UART (1. Моя демонстрационная плата тактируется от источника сигнала 50Mhz. 843198Mhz) и, по-приколу, сформировал частоту 300Mhz (выход c1 PLL) для тактирования имитации схемы обработки информации. 8432Mhz, реально 1.

Главный модуль (Verilog)

//
// Т.к. прием и передача данных через UART синхронизируется тактовой частотой UART,
// а обработка данных синхронизируется тактовой частотой FPGA, то нужно использовать
// для каждого модуля FIFO IP CORE типа DCFIFO.
//
//NB!
// Не забываем в SDC-файле прописывать соответствующие внутренние частоты!
// Иначе получаем зверские эффекты (типа часть внутри блока if выполнилась,
// а часть нет).
module uart( input wire clk50mhz, // тактовая частота 50Mhz input wire nreset, // инверсный сигнал сброса input wire rx, // входной сигнал UART output wire tx, // выходной сигнал UART output wire overflow ); // Тактовая частота 1.8432Mhz (реально 1.843198Mhz) wire clk_1843200;
// Тактовая частота 1.2288Mhz (реально 1.228799Mhz)
//wire clk_1228800; // Внутренняя тактовая частота 300Mhz, сформированная PLL
wire clk300mhz; // Синтезируем тактовые частоты для UART
uart_pll pll50mhz(.inclk0(clk50mhz), .c0(clk_1843200) /*, .c1(clk_1228800)*/, .c1(clk300mhz)); // Скорость UART 38400
// Делитель (1843200/38400)/4 = 12 ('b1100). // Скорость UART 57600
// Делитель (1843200/57600)/4 = 8 // Скорость UART 115200
// Делитель (1843200/115200)/4 = 4 // Скорость UART 230400
// Делитель (1843200/230400)/4 = 2 // Скорость UART 460800
// Делитель (1843200/460800)/4 = 1 (т.е. делитель вообще не нужен!) // Тактовая частота для UART
wire uart_baud4;
// Подключаем его в схему
// Значение делителя .data должно быть на 1 меньше требуемого делителя. Тогда
// период сигнала uart_baud4 будет равен значению .clock/делитель
// Длительность высокого уровня сигнала uart_baud4 будет равна одному такту .clock
uart_osc uart_osc_1(.clock(clk_1843200), .data(5'd2/*5'd4*//*5'd12*/-5'd1), .sload(uart_baud4), .cout(uart_baud4));
//wire uart_baud4 = clk_1843200; // Входной сигнал после мажоритарного фильтра wire rxf; // Подключаем мажоритарный фильтр на входной сигнал
mfilter mfilter_rx(.clk(clk50mhz /*clk_1843200*/), .in(rx), .out(rxf)); //wire rxf = rx; // Подключаем модуль приемника
wire[7:0] rxdata;
wire rxready;
wire error;
uart_rx uart_rx_1(.nreset(nreset), .clk(uart_baud4), .rx(rxf), .rdclk(clk300mhz /*clk50mhz*/ /*clk_1843200*/), .rxdata(rxdata), .ready(rxready), .error(error)); wire[7:0] txdata;
// Сигнал, индицирующий отсутствие данных, ожидающих передачи
wire txnone;
// Сигнал, индицирующий готовность передатчика принять новые данные
wire fetch; wire full; // Буферирование принятых данных
// Запись тактируется сигналом uart_baud4
// Чтение тактируется сигналом clk50mhz
uart_fifo_rx uart_fifo_rx_1(.data(rxdata), .rdclk(clk300mhz /*clk50mhz*/ /*clk_1843200*/ /*uart_baud4*/), .rdreq(fetch), .wrclk(clk300mhz /*clk50mhz*/ /*clk_1843200*/ /*uart_baud4*/), .wrreq(rxready), .rdempty(txnone), .q(txdata), .wrfull(full)); assign overflow = ~error; uart_tx uart_tx_1(.nreset(nreset), .clk(uart_baud4), .rdclk(clk300mhz /*clk50mhz*/ /*clk_1843200*/), .txdata(txdata), .write(~txnone), .fetch(fetch), .tx(tx)); endmodule

Для тестирования использовался генератор трафика testcom от Zelax. К сожалению, имеющийся у меня адаптер USB/UART отказался работать со скоростями выше 230400BPS, так что все тестирование проводилось на этой скорости.

Результат тестирования с фильтрацией входного сигнала RX при помощи мажоритарного элемента

Тестирование с предварительной фильтрацией сигнала RX
Состояние сигналов, снятые при помощи Signal Tap
Сигналы приемника UART при отсутствии ошибок

А вот здесь мажоритарный элемент со входа был убран.

А что, как мне еще было имитировать произвольные ошибки при проверке схемы быстрой синхронизации?
Тестирование без предварительной фильтрации сигнала RX
Состояние сигналов, снятые при помощи Signal Tap
Сигналы при быстрой синхронизации приемника после обнаружения ошибки

Примечание

Извините, курсов по Quartus я не проходил и вопросы задавать было некому. На что сам наткнулся и о чем предупреждаю других начинающих ПЛИСоводов: обязательно создавайте в проекте SDC-файл и описывайте в нем тактовые частоты. Да, проект собирается и без него, правда возможно появление предупреждений, если синтезатор не смог определить временные характеристики тактирования. Я их сначала игнорировал, пока не убил полдня на определение проблемы, почему у меня в модуле приемника при выполнении кода

if(rx == 1'b0) begin busy <= 1'b1; d <= 10'b1111111111; cnt <= 2'd0; idle <= 1'b0;
end else begin

сигналы busy и idle устанавливались правильно, а вот содержимое регистра d иногда не изменялось.

Приложение: SDC-файл к проекту

set_time_format -unit ns -decimal_places 3
# Тактовая частота 50Mhz, (50/50 duty cycle)
create_clock -name {clk50mhz} -period 20.000 -waveform { 0.000 10.000 } ############################################################################## Now that we have created the custom clocks which will be base clocks,# derive_pll_clock is used to calculate all remaining clocks for PLLs
derive_pll_clocks -create_base_clocks
derive_clock_uncertainty # Сигналы от PLL софтина умеет считать сама?
# altpll_component.clk0_divide_by = 15625,
# altpll_component.clk0_duty_cycle = 50,
# altpll_component.clk0_multiply_by = 576,
# altpll_component.clk0_phase_shift = "0",
#create_generated_clock -name clk_1843200 -source [get_ports {clk50mhz}] -divide_by 15625 -multiply_by 576 -duty_cycle 50 -phase 0 -offset 0 # Для baudrate=38400
# Сигнал активен в течении 1/4 цикла, т.е. duty=(1/4)*100=25%
#create_generated_clock -name uart_baud4 -source [get_nets {pll50mhz|altpll_component|auto_generated|wire_pll1_clk[0]}] -divide_by 12 -duty_cycle 25 [get_nets {uart_osc_1|LPM_COUNTER_component|auto_generated|counter_reg_bit[0]}] # Для baudrate=230400
# Сигнал активен в течении 1/4 цикла, т.е. duty=(1/4)*100=50%
create_generated_clock -name uart_baud4 -source [get_nets {pll50mhz|altpll_component|auto_generated|wire_pll1_clk[0]}] -divide_by 2 -duty_cycle 25 [get_nets {uart_osc_1|LPM_COUNTER_component|auto_generated|counter_reg_bit[0]}] # Для baudrate=460800
# Делитель равен 1, используется непосредственно выход PLL, поэтому описание дополнительной частоты не требуется.

Список внешних ссылок

  1. Универсальный асинхронный приёмопередатчик (ВикипедиЯ)
  2. Мажоритарный элемент (ВикипедиЯ)
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть