- PVSM.RU - https://www.pvsm.ru -

Наконец-то у меня дошли руки до изучения ПЛИС. А то как-то неправильно получается: драйвера на железо под Linux пишу, микроконтроллеры программирую, схемы читаю (и немного проектирую), надо расти дальше.
Так как мигать светодиодами мне показалось не интересно, то решил сделать простенькую вещь. А именно написать модули приемника и передатчика для UART, объединить их внутри FPGA (заодно понять как использовать IP Core), ну и протестировать это все на реальном железе.
Сразу говорю, что сделать универсальное параметризированное ядро задачи не стояло. Это просто тестовый проект, на предмет «пощупать, что такое FPGA и как с ней общаться».
Итак, начнем с приемника. Алгоритм достаточно хорошо описан [1], поэтому повторю здесь только основные его моменты.
Сразу отмечу, что тактировать сигнал чтения ready от тактового сигнала clk я не хотел (неожиданно, да?), чтобы не завязавать скорость последующей обработки данных на скорость обмена по UART. Аналогичная реализация и в модуле передатчика (см. ниже [2]). А тестовая связка модулей приемника и передатчика сделана на основе IP Core FIFO от Intel, причем с возможностью имитации различных скоростей для потребителя и генератора данных.
//
// Блок приемника 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 = { d[9], d[0] };
// Признак завершения приема.
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 является асинхронным и (возможно) нестабильным, в главном модуле [3] перед модулем приемника был подключен мажоритарный элемент [4]. Элемент также написан на 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 и т.п.) я и сам знаю. Но для целей проверки функциональности они не существенны.
Моя демонстрационная плата тактируется от источника сигнала 50Mhz. Поэтому в главном модуле я использовал PLL, на выходе C0 которого сформировал частоту для работы c UART (1.8432Mhz, реально 1.843198Mhz) и, по-приколу, сформировал частоту 300Mhz (выход c1 PLL) для тактирования имитации схемы обработки информации.
//
// Т.к. прием и передача данных через 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, так что все тестирование проводилось на этой скорости.


Извините, курсов по 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 иногда не изменялось.
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, поэтому описание дополнительной частоты не требуется.
Список внешних ссылок
Автор: Vedga
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/296357
Ссылки в тексте:
[1] хорошо описан: #UART
[2] см. ниже: #TRANSMITTER
[3] главном модуле: #MAIN
[4] мажоритарный элемент: #FILTER
[5] Универсальный асинхронный приёмопередатчик (ВикипедиЯ): https://ru.wikipedia.org/wiki/%D0%A3%D0%BD%D0%B8%D0%B2%D0%B5%D1%80%D1%81%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%B0%D1%81%D0%B8%D0%BD%D1%85%D1%80%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BF%D1%80%D0%B8%D1%91%D0%BC%D0%BE%D0%BF%D0%B5%D1%80%D0%B5%D0%B4%D0%B0%D1%82%D1%87%D0%B8%D0%BA
[6] Мажоритарный элемент (ВикипедиЯ): https://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D0%B6%D0%BE%D1%80%D0%B8%D1%82%D0%B0%D1%80%D0%BD%D1%8B%D0%B9_%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82
[7] Источник: https://habr.com/post/427011/?utm_campaign=427011
Нажмите здесь для печати.