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

от автора

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

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

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

  • Main
  • UART
  • UART_RX
  • UART_TX

Начнем с модуля 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_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:

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 мегагерц.

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

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

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

ссылка на оригинал статьи https://habrahabr.ru/post/278005/