Реализация стабильного UART, со скоростью 921600 baud и более, на языке Verilog под ПЛИС.

Всем привет. Пару недель назад я начал потихоньку изучать программирование под ПЛИС. Для этих целей мною была заказана у китайцев самая дешевая плата на основе Altera Max II EPM240T100C5N чипа. Установив Quartus v15, стал изучать Verilog стандарта 2001 года. Наморгавшись светодиодами решил попробовать реализовать какой-нибудь протокол передачи данных. Естественно им стал UART :) Посмотрев на чужие примеры в сети, мне не очень понравилось излишнее нагромождение логики, множество дополнительных счетчиков, а главное, проблемы с синхронизацией в приемнике и, как следствие, не стабильность работы на высоких скоростях. Конечно можно найти и качественные реализации, полностью конфигурируемые, да и вообще, с «идеальным кодом», но так не будет ни какого спортивного интереса.
И так, задача стояла реализовать максимально компактный, стабильный и простой 8-ми битный асинхронный приемопередатчик с 1-м стартовым и 1-м стоповым битом. Одним словом — классика. Но как оказалось задача не такая уж тривиальная, какой она была на первый взгляд. Реализовав приемник и передатчик буквально за один вечер, мне пришлось потратить еще два, что-бы заставить логику микросхемы не проглатывать, корректно принимать и отсылать поток байт, без ошибок.
Основываясь на критике и пожеланиях в комментариях на habrahabr.ru, мною была проведена работа над ошибками, и в статье представлена уже вторая реализация данного модуля. Прием и отправка данных были переведены на сдвиговые регистры, добавлена мажоритарная схема на вход RX из трех элементов, избавился от блокирующих присваиваний в синхронных блоках и счетчики тактового сигнала UART считают от максимального значения к нулю.

Файлы проекта:
- Main
- UART
- UART_TX
- UART_RX
- RXMajority3Filter
Начнем с модуля UART_TX:
UART_TX.v
module UART_TX #
(
parameter CLOCK_FREQUENCY = 50_000_000,
parameter BAUD_RATE = 9600
)
(
input clockIN,
input nTxResetIN,
input [7:0] txDataIN,
input txLoadIN,
output wire txIdleOUT,
output wire txReadyOUT,
output wire txOUT
);
localparam HALF_BAUD_CLK_REG_VALUE = (CLOCK_FREQUENCY / BAUD_RATE / 2 - 1);
localparam HALF_BAUD_CLK_REG_SIZE = $clog2(HALF_BAUD_CLK_REG_VALUE);
reg [HALF_BAUD_CLK_REG_SIZE-1:0] txClkCounter = 0;
reg txBaudClk = 1'b0;
reg [9:0] txReg = 10'h001;
reg [3:0] txCounter = 4'h0;
assign txReadyOUT = !txCounter[3:1];
assign txIdleOUT = txReadyOUT & (~txCounter[0]);
assign txOUT = txReg[0];
always @(posedge clockIN) begin : tx_clock_generate
if(txIdleOUT & (~txLoadIN)) begin
txClkCounter <= 0;
txBaudClk <= 1'b0;
end
else if(txClkCounter == 0) begin
txClkCounter <= HALF_BAUD_CLK_REG_VALUE;
txBaudClk <= ~txBaudClk;
end
else begin
txClkCounter <= txClkCounter - 1'b1;
end
end
always @(posedge txBaudClk or negedge nTxResetIN) begin : tx_transmit
if(~nTxResetIN) begin
txCounter <= 4'h0;
txReg[0] <= 1'b1;
end
else if(~txReadyOUT) begin
txReg <= {1'b0, txReg[9:1]};
txCounter <= txCounter - 1'b1;
end
else if(txLoadIN) begin
txReg <= {1'b1, txDataIN[7:0], 1'b0};
txCounter <= 4'hA;
end
else begin
txCounter <= 4'h0;
end
end
endmodule
Разберем все по порядку:
module UART_TX #
(
parameter CLOCK_FREQUENCY = 50_000_000,
parameter BAUD_RATE = 9600
)
(
input clockIN,
input nTxResetIN,
input [7:0] txDataIN,
input txLoadIN,
output wire txIdleOUT,
output wire txReadyOUT,
output wire txOUT
);
Параметры CLOCK_FREQUENCY и BAUD_RATE это частота кварцевого резонатора и частота UART передатчика соответственно.
Входящие порты:
clockIN — порт тактового сигнала с кварцевого резонатора.
nTxResetIN — порт сброса по отрицательному фронту.
txDataIN — восьмибитная шина данных.
txLoadIN — порт начала передачи данных.
Исходящие порты:
txIdleOUT — порт «простоя» передатчика, выставляется в лог. 1 при полном завершении цикла передачи байта данных, если на порту txLoadIN не будет присутствовать лог. 1.
txReadyOUT — порт, лог. 1 на котором, будет означать что стоповый бит был отправлен, и можно загружать новые данные.
txOUT — порт последовательной передачи исходящих данных, который нужно назначить на ножку ПЛИС.
localparam HALF_BAUD_CLK_REG_VALUE = (CLOCK_FREQUENCY / BAUD_RATE / 2 - 1);
localparam HALF_BAUD_CLK_REG_SIZE = $clog2(HALF_BAUD_CLK_REG_VALUE);
reg [HALF_BAUD_CLK_REG_SIZE-1:0] txClkCounter = 0;
reg txBaudClk = 1'b0;
reg [9:0] txReg = 10'h001;
reg [3:0] txCounter = 4'h0;
assign txReadyOUT = !txCounter[3:1];
assign txIdleOUT = txReadyOUT & (~txCounter[0]);
assign txOUT = txReg[0];
Локальный параметр HALF_BAUD_CLK_REG_VALUE — значение счетчика-делителя частоты полупериода тактового сигнала UART. Вычисляется по формуле CLOCK_FREQUENCY / BAUD_RATE / 2 — 1.
Локальный параметр HALF_BAUD_CLK_REG_SIZE — разрядность этого самого счетчика. Вычисляется чудесной функцией $clog2 — логарифмом по основанию 2 от значения параметра HALF_BAUD_CLK_REG_VALUE.
Регистры reg:
txClkCounter — счетчик-делитель частоты тактового сигнала.
txBaudClk — тактовый сигнал для передатчика.
txReg — сдвиговый регистр в который будут записываться байт данных, стартовый и стоповый бит.
txCounter — счетчик количества отправленных бит.
Провода wire:
txReadyOUT назначен непрерывным соединением через логическое отрицание на 4-й, 3-й и 2-й бит регистра txCounter Принимает состояние лог. 1 при достижении счетчиком txCounter значения 1 или 0.
txIdleOUT назначен непрерывным соединением на txReadyOUT и через логический примитив AND на инвертированный 1-й бит регистра txCounter. Принимает состояние лог. 1 при достижении счетчиком txCounter значения 0.
txOUT назначен непрерывным соединением на 1-й бит регистра txReg
Передача данных:
always @(posedge txBaudClk or negedge nTxResetIN) begin : tx_transmit
if(~nTxResetIN) begin
txCounter <= 4'h0;
txReg[0] <= 1'b1;
end
else if(~txReadyOUT) begin
txReg <= {1'b0, txReg[9:1]};
txCounter <= txCounter - 1'b1;
end
else if(txLoadIN) begin
txReg <= {1'b1, txDataIN[7:0], 1'b0};
txCounter <= 4'hA;
end
else begin
txCounter <= 4'h0;
end
end
По отрицательному фронту на порту nTxResetIN, который проверяется в первом условии, на первом бите регистра txReg выставляется лог. 1, а регистр txCounter принимает значение 0, что дает на выходах txIdleOUT и txReadyOUT и txOUT лог. 1.
В противном случае по положительному фронту на порту txBaudClk проверяется значение сигнала порта txReadyOUT, и, при лог. 0, содержимое регистра txReg сдвигается в сторону младших бит, а счетчик txCounter уменьшается на единицу, и при достижении значения 1 на выходе txReadyOUT будет установлена лог. 1.
Иначе по положительному фронту на порту txBaudClk проверяется сигнал порта txLoadIN, и, при лог. 1, в регистр txReg попадают значение со входа txDataIN стартовый и стоповый бит, счетчик txCounter принимает значение 10 (4'hA), что даст отрицательный фронт на выходах txIdleOUT и txReadyOUT и txOUT — что будет сигнализировать начало передачи данных (стартовый бит).
Иначе регистр txCounter принимает значение 0, и на выходе txIdleOUT появляется лог. 1.
Стоит отметить что по данной логике при лог. 1 на txLoadIN данные будут постоянно забираться со входа txDataIN в регистр txReg и последовательно передаваться на выход txOUT. Т.е. для прекращения передачи пакета данных, нужно сбросить txLoadIN в лог. 0 до того, как будет полностью передан стоповый бит. Лучший способ — это сброс txLoadIN по отрицательному фронту на порту txReadyOUT. Прервать процесс передачи байта данных логическим нулем на txLoadIN нельзя. Для этого можно использовать nTxResetIN.
Формирование тактового сигнала передатчика:
always @(posedge clockIN) begin : tx_clock_generate
if(txIdleOUT & (~txLoadIN)) begin
txClkCounter <= 0;
txBaudClk <= 1'b0;
end
else if(txClkCounter == 0) begin
txClkCounter <= HALF_BAUD_CLK_REG_VALUE;
txBaudClk <= ~txBaudClk;
end
else begin
txClkCounter <= txClkCounter - 1'b1;
end
end
По положительному фронту тактового сигнала на порту clockIN в первом условии проверяется лог. 1 на txIdleOUT и лог. 0 на txLoadIN, и при выполнении условия регистр txClkCounter сбрасывается в 0, и на тактовом сигнале txBaudClk устанавливается лог. 0. Т.е. тем самым мы гарантируем что при лог. 1 на txDataIN передатчик начнет передачу данных уже по следующему положительному фронту clockIN.
В противном случае txClkCounter проверяется на достижение значения 0, и при выполнении условия в txClkCounter будет записано значение параметра HALF_BAUD_CLK_REG_VALUE а txBaudClk инвертирует свое состояние.
Иначе txClkCounter уменьшит свое значение на 1.
Временная диаграмма сигналов модуля UART_TX:

Модуль UART_RX:
UART_RX.v
module UART_RX #
(
parameter CLOCK_FREQUENCY = 50_000_000,
parameter BAUD_RATE = 9600
)
(
input clockIN,
input nRxResetIN,
input rxIN,
output wire rxIdleOUT,
output wire rxReadyOUT,
output wire [7:0] rxDataOUT
);
localparam HALF_BAUD_CLK_REG_VALUE = (CLOCK_FREQUENCY / BAUD_RATE / 2 - 1);
localparam HALF_BAUD_CLK_REG_SIZE = $clog2(HALF_BAUD_CLK_REG_VALUE);
reg [HALF_BAUD_CLK_REG_SIZE-1:0] rxClkCounter = 0;
reg rxBaudClk = 1'b0;
reg [9:0] rxReg = 10'h000;
wire rx;
assign rxIdleOUT = ~rxReg[0];
assign rxReadyOUT = rxReg[9] & rxIdleOUT;
assign rxDataOUT[7:0] = rxReg[8:1];
RXMajority3Filter rxFilter
(
.clockIN(clockIN),
.rxIN(rxIN),
.rxOUT(rx)
);
always @(posedge clockIN) begin : rx_clock_generate
if(rx & rxIdleOUT) begin
rxClkCounter <= HALF_BAUD_CLK_REG_VALUE;
rxBaudClk <= 0;
end
else if(rxClkCounter == 0) begin
rxClkCounter <= HALF_BAUD_CLK_REG_VALUE;
rxBaudClk <= ~rxBaudClk;
end
else begin
rxClkCounter <= rxClkCounter - 1'b1;
end
end
always @(posedge rxBaudClk or negedge nRxResetIN) begin : rx_receive
if(~nRxResetIN) begin
rxReg <= 10'h000;
end
else if(~rxIdleOUT) begin
rxReg <= {rx, rxReg[9:1]};
end
else if(~rx) begin
rxReg <= 10'h1FF;
end
end
endmodule
module UART_RX #
(
parameter CLOCK_FREQUENCY = 50_000_000,
parameter BAUD_RATE = 9600
)
(
input clockIN,
input nRxResetIN,
input rxIN,
output wire rxIdleOUT,
output wire rxReadyOUT,
output wire [7:0] rxDataOUT
);
Во многом похож на модуль UART_TX.
Входящие порты:
clockIN и nRxResetIN имеют те-же значения что и в модуле UART_RX
rxIN — входящий порт последовательной передачи данных, который нужно назначить на ножку ПЛИС.
Исходящие порты:
rxIdleOUT — порт «простоя» приемника, выставляется в лог. 1 при полном завершении цикла приема байта данных.
rxReadyOUT — порт готовности приемника. При переходе в лог. 1 показывает, что был принят байт данных, который завершился стоповым битом (лог. 1). Переходит в состояние лог. 0 при лог. 0 на порту nRxResetIN или при начале приема следующего байта данных.
rxDataOUT — восьмибитная шина принятых данных.
localparam HALF_BAUD_CLK_REG_VALUE = (CLOCK_FREQUENCY / BAUD_RATE / 2 - 1);
localparam HALF_BAUD_CLK_REG_SIZE = $clog2(HALF_BAUD_CLK_REG_VALUE);
reg [HALF_BAUD_CLK_REG_SIZE-1:0] rxClkCounter = 0;
reg rxBaudClk = 1'b0;
reg [9:0] rxReg = 10'h000;
wire rx;
assign rxIdleOUT = ~rxReg[0];
assign rxReadyOUT = rxReg[9] & rxIdleOUT;
assign rxDataOUT[7:0] = rxReg[8:1];
Регистры reg:
rxClkCounter — счетчик-делитель частоты тактового сигнала.
rxBaudClk — тактовый сигнал для приемника.
rxReg — сдвиговый регистр, который хранит 8 бит принятых данных, стартовый и стоповый бит.
Провода wire:
rx — сигнал входящих последовательных данных, пропущенный через мажоритарную логику модуля RXMajority3Filter.
rxIdleOUT непрерывно назначен на на инвертированный 1-й бит регистра rxReg. Принимает лог. 1 при окончании приема данных, когда в регистр rxReg[0] будет записан стоповый бит.
rxReadyOUT непрерывно назначен на 10-й бит регистра rxReg и rxIdleOUT через логический примитив AND. Принимает лог. 1 если прием данных был завершен и в регистре rxReg 10-й бит принял значение лог. 1 (стоповый бит).
rxDataOUT назначен с 9 по 1 биты регистра rxReg.
Прием данных:
always @(posedge rxBaudClk or negedge nRxResetIN) begin : rx_receive
if(~nRxResetIN) begin
rxReg <= 10'h000;
end
else if(~rxIdleOUT) begin
rxReg <= {rx, rxReg[9:1]};
end
else if(~rx) begin
rxReg <= 10'h1FF;
end
end
По отрицательному фронту на порту nRxResetIN, будет выполнено первое условие, и rxReg сбросится в 0, что установит лог. 0 на порту rxReadyOUT и лог. 1 на rxIdleOUT.
В противном случае при лог. 0 на порту rxIdleOUT содержимое регистра rxReg сдвигается в сторону младших бит, а в старший бит будет записано текущее состояние сигнала rx.
Иначе лог. 0 сигнала rx будет означать начало передачи данных (стартовый бит), и в регистр rxReg во все биты кроме старшего (стартовый бит) будут записаны единицы (10'h1FF).
Формирование тактового сигнала приемника:
always @(posedge clockIN) begin : rx_clock_generate
if(rx & rxIdleOUT) begin
rxClkCounter <= HALF_BAUD_CLK_REG_VALUE;
rxBaudClk <= 0;
end
else if(rxClkCounter == 0) begin
rxClkCounter <= HALF_BAUD_CLK_REG_VALUE;
rxBaudClk <= ~rxBaudClk;
end
else begin
rxClkCounter <= rxClkCounter - 1'b1;
end
end
Назначение второго и третьего условия идентично условию из модуля UART_TX — формирование тактового сигнала для приемника.
В первом-же условии проверяются лог. 1 сигнала rx и лог. 1 сигнала rxIdleOUT, и при выполнении условия в txClkCounter будет записано значение параметра HALF_BAUD_CLK_REG_VALUE, а на rxBaudClk будет установлен лог. 0.
Т.е. при появлении лог. 0 (стартовый бит) на порту rx, счетчик отсчитает половину периода тактового сигнала приемника, и только после этого будет начат прием данных.
Временная диаграмма сигналов модуля UART_RX:

Модуль RXMajority3Filter:
RXMajority3Filter.v
module RXMajority3Filter
(
input clockIN,
input rxIN,
output wire rxOUT
);
reg [2:0] rxLock = 3'b111;
assign rxOUT = (rxLock[0] & rxLock[1]) | (rxLock[0] & rxLock[2]) | (rxLock[1] & rxLock[2]);
always @(posedge clockIN) begin
rxLock <= {rxIN, rxLock[2:1]};
end
endmodule
Представляет собой реализацию мажоритарного элемента на сдвиговом регистре из трех элементов.
Модуль UART:
UART.v
module UART #
(
parameter CLOCK_FREQUENCY = 50_000_000,
parameter BAUD_RATE = 9600
)
(
input clockIN,
input nTxResetIN,
input [7:0] txDataIN,
input txLoadIN,
output wire txIdleOUT,
output wire txReadyOUT,
output wire txOUT,
input nRxResetIN,
input rxIN,
output wire rxIdleOUT,
output wire rxReadyOUT,
output wire [7:0] rxDataOUT
);
defparam uart_tx.CLOCK_FREQUENCY = CLOCK_FREQUENCY;
defparam uart_tx.BAUD_RATE = BAUD_RATE;
UART_TX uart_tx
(
.clockIN(clockIN),
.nTxResetIN(nTxResetIN),
.txDataIN(txDataIN),
.txLoadIN(txLoadIN),
.txIdleOUT(txIdleOUT),
.txReadyOUT(txReadyOUT),
.txOUT(txOUT)
);
defparam uart_rx.CLOCK_FREQUENCY = CLOCK_FREQUENCY;
defparam uart_rx.BAUD_RATE = BAUD_RATE;
UART_RX uart_rx
(
.clockIN(clockIN),
.nRxResetIN(nRxResetIN),
.rxIN(rxIN),
.rxIdleOUT(rxIdleOUT),
.rxReadyOUT(rxReadyOUT),
.rxDataOUT(rxDataOUT)
);
endmodule
Просто объединяет два модуля UART_RX и UART_TX в единое целое, пробрасывая входящие и исходящие сигналы, и значения параметров частоты кварцевого резонатора и частоты UART передатчика.
И собственно модуль верхнего уровня Main:
Main.v
module Main
(
input wire clockIN,
input wire uartRxIN,
output wire uartTxOUT
);
defparam uart.CLOCK_FREQUENCY = 50_000_000;
defparam uart.BAUD_RATE = 921600;
reg [7:0] txData;
reg txLoad = 1'b0;
wire txReset = 1'b1;
wire rxReset = 1'b1;
wire [7:0] rxData;
wire txIdle;
wire txReady;
wire rxIdle;
wire rxReady;
UART uart
(
.clockIN(clockIN),
.nTxResetIN(txReset),
.txDataIN(txData),
.txLoadIN(txLoad),
.txIdleOUT(txIdle),
.txReadyOUT(txReady),
.txOUT(uartTxOUT),
.nRxResetIN(rxReset),
.rxIN(uartRxIN),
.rxIdleOUT(rxIdle),
.rxReadyOUT(rxReady),
.rxDataOUT(rxData)
);
always @(posedge rxReady or negedge txReady) begin
if(~txReady)
txLoad <= 1'b0;
else if(rxReady) begin
txLoad <= 1'b1;
txData <= rxData;
end
end
endmodule
Является по сути простым «эхо» тестом.
По положительному фронту на порту rxReady входящие данные будут записаны в регистр txData, который назначен на вход txDataIN передатчика, и регистр txLoad, который назначен на вход передатчика txLoadIN будет выставлен в лог. 1, для начала передачи.
По отрицательному фронту на порту txReady, регистр txLoad примет значение лог. 0.
Данный модуль был протестирован на плате с Altera Max II EPM240T100C5N чипом и кварцевым резонатором с частотой 50 мегагерц, со скоростью UART в 921600 baud (максимальная скорость, которую поддерживает мой USB-UART переходник).
По стандарту, для приемника, частота сэмплирования стартового бита должна быть минимум в 16 раз больше частоты UART. Так что для стабильной работы модуля при 921600 baud rate, частота кварцевого резонатора должна быть не ниже 921600 * 16 = 14'745'600 герц. Например пойдет кристалл на 16 мегагерц.
Также желательно поставить подтягивающий резистор на вход приемника.
Как обычно, любые советы по оптимизации и улучшении приветствуются :)
Скачать обновленные файлы можно тут.
11 комментариев
Я обычно использую UART из состава picoblaze. Он только для xilinx, но очень хорошо заточен под их архитектуру (почти не занимает ресурсов) и имеет FIFO.
Я уже переписал код, с учетом всех «пожеланий» с хабра. Модуль похудел на 25%. Перевел все на сдвиговые регистры, добавил этот самый мажоритарный элемент для rxIN, счетчики теперь идут сверху вниз к нулю. Короче на днях буду переписывать статью.
Уарт в исходниках, но в специфичных — он собран из шифтрегистров и прочих ЛУТов, и, именно поэтому, такой маленький.
Использовать отдельно от процессора не только можно, но и нужно. Он не имеет жесткой заточенности под шины, этим и удобен.
Почему именно:
а не
Я уж, наверное, с пол года не ковырял MAX и Verilog :)