Пару недель назад я начал потихоньку изучать программирование под ПЛИС. Для этих целей мною была заказана у китайцев самая дешевая плата на основе Altera Max II EPM240T100C5N чипа. Установив Quartus v15, стал изучать Verilog стандарта 2001 года. Наморгавшись светодиодами решил попробовать реализовать какой-нибудь протокол передачи данных. Естественно им стал UART. Посмотрев на чужие примеры в сети, понял, что мне не очень нравится излишнее нагромождение логики, множество дополнительных счетчиков, а главное — проблемы с синхронизацией в приемнике и, как следствие, нестабильность работы на высоких скоростях. Конечно, можно найти и качественные реализации, полностью конфигурируемые, да и вообще с «идеальным кодом», но так не будет никакого спортивного интереса.
Итак, стояла задача реализовать максимально компактный, стабильный и простой 8-ми битный асинхронный приемопередатчик с 1-м стартовым и 1-м стоповым битом. Одним словом — классика. Но как оказалось, задача не такая уж тривиальная, какой она была на первый взгляд. Реализовав приемник и передатчик буквально за один вечер, мне пришлось потратить еще два, чтобы заставить логику микросхемы не проглатывать, корректно принимать и отсылать поток байт, без ошибок.
Далее я приведу свою реализацию, и попытаюсь объяснить что, как и зачем. Весь проект состоит из 4-х модулей:
- Main
- UART
- UART_RX
- UART_TX
Начнем с модуля UART_TX:
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:
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:
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:
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 байт, т.е. 1 посылку, можно ли как то переделать ваш проект или дополнить, что бы принимать несколько байт?