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

в 7:39, , рубрики: Altera, cpld, fpga, uart, Verilog, программирование микроконтроллеров

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

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

Итак, стояла задача реализовать максимально компактный, стабильный и простой 8-ми битный асинхронный приемопередатчик с 1-м стартовым и 1-м стоповым битом. Одним словом — классика. Но как оказалось, задача не такая уж тривиальная, какой она была на первый взгляд. Реализовав приемник и передатчик буквально за один вечер, мне пришлось потратить еще два, чтобы заставить логику микросхемы не проглатывать, корректно принимать и отсылать поток байт, без ошибок.

Далее я приведу свою реализацию, и попытаюсь объяснить что, как и зачем. Весь проект состоит из 4-х модулей:

  • Main
  • UART
  • UART_RX
  • UART_TX

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

Начнем с модуля 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_COMPARE_REG_VALUE = (CLOCK_FREQUENCY / BAUD_RATE / 2 - 1); localparam HALF_BAUD_CLK_COMPARE_REG_SIZE = $clog2(HALF_BAUD_CLK_COMPARE_REG_VALUE); reg [HALF_BAUD_CLK_COMPARE_REG_SIZE-1:0] txClkCounter = 0; reg txBaudClk = 1'b0; reg [7:0] txReg = 8'h00; reg [3:0] txIndex = 4'hA; reg txPin = 1'b1; wire [8:0] txData = {1'b1, txReg[7:0]}; assign txReadyOUT = (txIndex[3] & (txIndex[1] | txIndex[0])); //4'b1xx1 (4'h9) || 4'b1x1x (4'hA) assign txIdleOUT = (txIndex[3] & txIndex[1]); //4'b1x1x (4'hA) assign txOUT = txPin; always @(posedge clockIN) begin : tx_clock_generate if(txIdleOUT & (~txLoadIN)) begin txClkCounter <= HALF_BAUD_CLK_COMPARE_REG_VALUE; txBaudClk <= 1'b0; end else if(txClkCounter == HALF_BAUD_CLK_COMPARE_REG_VALUE) begin txClkCounter <= 0; txBaudClk <= ~txBaudClk; end else begin txClkCounter <= txClkCounter + 1'b1; end end always @(posedge txBaudClk or negedge nTxResetIN) begin : tx_transmit if(~nTxResetIN) begin txIndex <= 4'hA; end else if(~txReadyOUT) begin txPin = txData[txIndex]; txIndex = txIndex + 1'b1; end else if(txLoadIN) begin txReg[7:0] <= txDataIN[7:0]; txIndex <= 4'h0; txPin <= 1'b0; end else begin txIndex <= 4'hA; 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_COMPARE_REG_VALUE = (CLOCK_FREQUENCY / BAUD_RATE / 2 - 1);
localparam HALF_BAUD_CLK_COMPARE_REG_SIZE  = $clog2(HALF_BAUD_CLK_COMPARE_REG_VALUE);

reg [HALF_BAUD_CLK_COMPARE_REG_SIZE-1:0] txClkCounter = 0;
reg txBaudClk = 1'b0;

reg [7:0] txReg   = 8'h00;
reg [3:0] txIndex = 4'hA;
reg       txPin = 1'b1;

wire [8:0] txData = {1'b1, txReg[7:0]};

assign txReadyOUT = (txIndex[3] & (txIndex[1] | txIndex[0])); //4'b1xx1 (4'h9) || 4'b1x1x (4'hA)
assign txIdleOUT  = (txIndex[3] & txIndex[1]); //4'b1x1x (4'hA)
assign txOUT      = txPin;

Локальный параметр HALF_BAUD_CLK_COMPARE_REG_VALUE — значение счетчика-делителя частоты полупериода тактового сигнала UART. Вычисляется по формуле CLOCK_FREQUENCY / BAUD_RATE / 2 — 1.

Локальный параметр HALF_BAUD_CLK_COMPARE_REG_SIZE — разрядность этого самого счетчика. Вычисляется чудесной функцией $clog2 — логарифмом по основанию 2 от значения параметра HALF_BAUD_CLK_COMPARE_REG_VALUE.

Регистры reg:

txClkCounter — счетчик-делитель частоты тактового сигнала.
txBaudClk — тактовый сигнал для передатчика.
txReg — здесь будет хранится байт данных на отправку.
txIndex — индекс текущего бита для отправки.
txPin — регистр, который хранит состояние порта исходящих последовательных данных.

Провода wire:

txData — шина данных, которые будут отправлены при передаче. Состоит из 8 бит данных регистра txReg (стартовый бит 0 будет послан отдельно, при лог. 1 на порту txLoadIN), и стопового бита 1.
txReadyOUT назначен непрерывным соединением на 1-й 2-й и 4-й бит регистра txIndex через два логических примитива AND и OR. Принимает состояние лог. 1 при достижении счетчиком txIndex значения 9 (4'h9) или 10 (4'hA).
txIdleOUT назначен непрерывным соединением на 2-й и 4-й бит регистра txIndex через логический примитив AND. Принимает состояние лог. 1 при достижении счетчиком txIndex значения 10 (4'hA).
txOUT назначен непрерывным соединением на регистр txPin

Передача данных:

always @(posedge txBaudClk or negedge nTxResetIN) begin : tx_transmit
	if(~nTxResetIN) begin
		txIndex <= 4'hA;
	end
	else if(~txReadyOUT) begin
		txPin = txData[txIndex];
		txIndex = txIndex + 1'b1;
	end
	else if(txLoadIN) begin
		txReg[7:0] <= txDataIN[7:0];
		txIndex <= 4'h0;
		txPin <= 1'b0;
	end
	else begin
		txIndex <= 4'hA;
	end
end

По отрицательному фронту на порту nTxResetIN, который проверяется в первом условии, регистр txIndex принимает значение 10 (4'hA), и на выходах txIdleOUT и txReadyOUT появляется лог. 1.

В противном случае по положительному фронту на порту txBaudClk проверяется значение сигнала порта txReadyOUT, и, при лог. 0, блокирующим присваиванием в регистр txPin попадает бит из txData по индексу txIndex, после чего счетчик txIndex увеличивается на единицу, и при достижении значения 9 (4'h9) на выходе txReadyOUT будет установлена лог. 1.

Иначе по положительному фронту на порту txBaudClk проверяется сигнал порта txLoadIN, и, при лог. 1, асинхронно в регистр txReg попадает значение со входа txDataIN, счетчик txIndex сбрасывается в 0 — что даст отрицательный фронт на выходах txIdleOUT и txReadyOUT, и регистр txPin будет сброшен в лог 0 — что будет сигнализировать начало передачи данных (стартовый бит).

Иначе регистр txIndex принимает значение 10 (4'hA), и на выходах txIdleOUT и txReadyOUT появляется лог. 1.

Стоит отметить что по данной логике при лог. 1 на txLoadIN данные будут постоянно забираться со входа txDataIN в регистр txReg и последовательно передаваться на выход txOUT. Т.е. для прекращения передачи пакета данных, нужно сбросить txLoadIN в лог. 0 до того, как будет полностью передан стоповый бит. Лучший способ — это сброс txLoadIN по отрицательному фронту на порту txReadyOUT. Прервать процесс передачи байта данных логическим нулем на txLoadIN нельзя. Для этого можно использовать nTxResetIN.

Формирование тактового сигнала передатчика:

always @(posedge clockIN) begin : tx_clock_generate
	if(txIdleOUT & (~txLoadIN)) begin
		txClkCounter <= HALF_BAUD_CLK_COMPARE_REG_VALUE;
		txBaudClk    <= 1'b0;
	end
	else if(txClkCounter == HALF_BAUD_CLK_COMPARE_REG_VALUE) begin
		txClkCounter <= 0;
		txBaudClk    <= ~txBaudClk;
	end
	else begin
		txClkCounter <= txClkCounter + 1'b1;
	end
end

По положительному фронту тактового сигнала на порту clockIN в первом условии проверяется лог. 1 на txIdleOUT и лог. 0 на txLoadIN, и при выполнении условия регистру txClkCounter присваивается максимальное значение счетчика HALF_BAUD_CLK_COMPARE_REG_VALUE, а на тактовом сигнале txBaudClk устанавливается лог. 0. Т.е. тем самым мы гарантируем что при лог. 1 на txDataIN передатчик начнет передачу данных уже по следующему положительному фронту clockIN.

В противном случае txClkCounter проверяется на совпадение с максимальным значением HALF_BAUD_CLK_COMPARE_REG_VALUE, и при выполнении условия txClkCounter будет сброшен в 0 а txBaudClk инвертирует свое состояние.

Иначе txClkCounter увеличит свое значение на 1.

Временная диаграмма сигналов модуля UART_TX:

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

Модуль 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_COMPARE_REG_VALUE = (CLOCK_FREQUENCY / BAUD_RATE / 2 - 1);
localparam HALF_BAUD_CLK_COMPARE_REG_SIZE  = $clog2(HALF_BAUD_CLK_COMPARE_REG_VALUE);

reg [HALF_BAUD_CLK_COMPARE_REG_SIZE-1:0] rxClkCounter = 0;
reg rxBaudClk = 1'b0;

reg [8:0] rxReg      = 9'h000;
reg [3:0] rxIndex    = 4'h9;

assign rxIdleOUT = (rxIndex[3] & rxIndex[0]); //4'b1xx1 || 4'h9
assign rxReadyOUT = (rxIdleOUT & rxReg[8]);
assign rxDataOUT[7:0] = rxReg[7:0];

always @(posedge clockIN) begin : rx_clock_generate
	if(rxIN & rxIdleOUT) begin
		rxClkCounter <= 0;
		rxBaudClk    <= 0;
	end
	else if(rxClkCounter == HALF_BAUD_CLK_COMPARE_REG_VALUE) begin
		rxClkCounter <= 0;
		rxBaudClk <= ~rxBaudClk;
	end
	else begin
		rxClkCounter <= rxClkCounter + 1'b1;
	end
end

always @(posedge rxBaudClk or negedge nRxResetIN) begin : rx_receive
	if(~nRxResetIN) begin
		rxReg[8] <= 1'b0;
		rxIndex  <= 4'h9;
	end
	else if(~rxIdleOUT) begin
		rxReg[rxIndex] = rxIN;
		rxIndex = rxIndex + 1'b1;
	end
	else if(~rxIN) begin
		rxIndex <= 4'h0;
	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_COMPARE_REG_VALUE = (CLOCK_FREQUENCY / BAUD_RATE / 2 - 1);
localparam HALF_BAUD_CLK_COMPARE_REG_SIZE  = $clog2(HALF_BAUD_CLK_COMPARE_REG_VALUE);

reg [HALF_BAUD_CLK_COMPARE_REG_SIZE-1:0] rxClkCounter = 0;
reg rxBaudClk = 1'b0;

reg [8:0] rxReg      = 9'h000;
reg [3:0] rxIndex    = 4'h9;

assign rxIdleOUT = (rxIndex[3] & rxIndex[0]); //4'b1xx1 || 4'h9
assign rxReadyOUT = (rxIdleOUT & rxReg[8]);
assign rxDataOUT[7:0] = rxReg[7:0];

Регистры reg:

rxClkCounter — счетчик-делитель частоты тактового сигнала.
rxBaudClk — тактовый сигнал для приемника.
rxReg — регистр, который хранит 8 бит принятых данных и последний 9-й стоповый бит.
rxIndex — индекс текущего бита приема данных.

Провода wire:

rxIdleOUT непрерывно назначен на 0-й и 4-й бит регистра rxIndex через логический примитив AND. Принимает лог. 1 при достижении счетчиком rxIndex значения 9 (4'h9).
rxReadyOUT непрерывно назначен на порт rxIdleOUT и 9-й бит регистра rxReg через логический примитив AND. Принимает лог. 1 если прием данных был завершен и в регистре rxReg 9-й бит принял значение лог. 1 (стоповый бит).
rxDataOUT назначен на регистр rxReg.

Прием данных:

always @(posedge rxBaudClk or negedge nRxResetIN) begin : rx_receive
	if(~nRxResetIN) begin
		rxReg[8] <= 1'b0;
		rxIndex  <= 4'h9;
	end
	else if(~rxIdleOUT) begin
		rxReg[rxIndex] = rxIN;
		rxIndex = rxIndex + 1'b1;
	end
	else if(~rxIN) begin
		rxIndex <= 4'h0;
	end
end

По отрицательному фронту на порту nRxResetIN, будет выполнено первое условие, и 9-й бит регистра rxReg сбросится в лог. 0, что установит лог. 0 на порту rxReadyOUT. А так-же в регистр rxIndex будет записано число 9 (4'h9), что установит линию rxIdleOUT в состояние лог. 1.

В противном случае при лог. 0 на порту rxIdleOUT блокирующим присваиванием в регистр rxReg под индексом rxIndex попадает состояние сигнала на порту rxIN, после чего счетчик rxIndex увеличивается на единицу, и при достижении значения 9 (4'h9) на выходе rxIdleOUT будет установлена лог. 1, и лог. 1 на выходе rxReadyOUT, если в 9-й бит регистра rxReg был принят стоповый бит (лог. 1).

Иначе лог. 0 на порту rxIN будет означать начало передачи данных (стартовый бит), и в регистр rxIndex будет записан 0.

Формирование тактового сигнала приемника:

always @(posedge clockIN) begin : rx_clock_generate
	if(rxIN & rxIdleOUT) begin
		rxClkCounter <= 0;
		rxBaudClk    <= 0;
	end
	else if(rxClkCounter == HALF_BAUD_CLK_COMPARE_REG_VALUE) begin
		rxClkCounter <= 0;
		rxBaudClk <= ~rxBaudClk;
	end
	else begin
		rxClkCounter <= rxClkCounter + 1'b1;
	end
end

Назначение второго и третьего условия идентично условию из модуля UART_TX — формирование тактового сигнала для приемника.

В первом-же условии проверяются лог. 1 сигнала rxIN и лог. 1 сигнала rxIdleOUT, и при выполнении условия счетчик rxClkCounter будет сброшен в 0, а на rxBaudClk будет установлен лог. 0.

Т.е. при появлении лог. 0 (стартовый бит) на порту rxIN, счетчик отсчитает половину периода тактового сигнала приемника, и только после этого будет начат прием данных.

Временная диаграмма сигналов модуля UART_RX:

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

Модуль 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 мегагерц.

Также желательно поставить подтягивающий резистор на вход приемника.

Любые советы по оптимизации и улучшении приветствуются.

Скачать файлы можно тут.

Автор: Hypnotriod

Источник


  1. Gleb:

    спасибо , очень полезно. Работает.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js