nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Мучаем 5576ХС4Т - часть 8 - передатчик UART

Часть 0 - покупаем, паяем, ставим драйвера и софт
Часть 1 - что это вообще за зверь?
Часть 2 - наша первая схема!
Часть 3 - кнопочки и лампочки
Часть 4 - делитель частоты
Часть 5 - подавление дребезга кнопки
Часть 6 - заканчиваем кнопочки и лампочки
Часть 7 - счетчики и жаба
Часть 8 - передатчик UART
Часть 9 - Hello, wolf!
Часть 'hA - приёмник UART
Часть 'hB - UART и жаба
Часть 'hC - полудуплексный UART.
Часть 'hD - МКО (МКИО, Mil-Std 1553) для бедных, введение.
Часть 'hE - приёмопередатчик МКО "из подручных материалов" (в процессе)
Часть 'hF - модуль передатчика МКО
Часть 'h10 - передатчик сообщений МКО
Часть 'h20 - работа с АЦП ADC124s051
Часть 'h21 - преобразование двоичного кода в двоично-десятичный (BCD)
Часть 'h22 - Bin2Bcd с последовательной выдачей данных
Часть 'h23 - перемножитель беззнаковых чисел с округлением
Часть 'h24 - перемножитель беззнаковых чисел, реализация
Часть 'h25 - передаём показания АЦП на компьютер
Часть 'h26 - работа над ошибками (быстрый UART)
Часть 'h27 - PNG и коды коррекции ошибок CRC32
Часть 'h28 - передатчик изображения PNG
Часть 'h29 - принимаем с ПЛИС изображение PNG
Часть 'h2A - ZLIB и коды коррекции ошибок Adler32
Часть 'h2B - ускоряем Adler32
Часть 'h2C - формирователь потока Zlib
Часть 'h2D - передаём сгенерированное PNG-изображение
Часть 'h2E - делим отрезок на равные части
Часть 'h2F - знаковые умножители, тысячи их!
Часть 'h30 - вычислитель множества Мандельброта
Часть 'h31 - ускоренные сумматоры
Часть 'h32 - ускоренные счётчики (делаем часы)
Часть 'h33 - ускоряем ВСЁ
Часть 'h34 - ускоренные перемножители
Часть 'h35 - умножители совсем просто
Часть 'h36 - уравновешенный четверичный умножитель


Ну что ж, мы почти созрели до Hello, world! Эта надпись должна посылаться на виртуальный COM-порт компьютера, реализованный на микросхемке cp2102, расположенной на отладочной плате. В этой части, правда, мы передадим всего один символ, а строку передадим только в следующей, когда научимся сохранять её во внутренней памяти ПЛИС.




Микросхема cp2102 соединена с ПЛИС двумя проводами: RXD и TXD - по одному данные передаются на ПЛИС, по другому - из неё. Уровень сигнала - 3.3 вольта, что соответствует питанию периферии ПЛИС.

Как назвать этот короткий канал длиной аж в 5 см - вопрос, терминологическая путаница страшная! Самое близкое слово - UART, Universal Asynchronous Receiver-Transmitter, хотя как явствует из самой расшифровки, мы должны называть UART отдельный блок в микроконтроллере или в ПЛИС, который передаёт и принимает данные! Сам канал, состоящий из отдельных проводов на приём и передачу, иногда называют RS232, хотя и это не верно, потому что напряжения не те. В стандарте RS232 определены уровни -3 .. -12 вольт для логической единицы и +3 .. +12 для логического нуля, тогда как у нас здесь самые обычные 0 для нуля и +3,3 для единицы. COM-порт - тоже мимо - в "честном" COM-порте реализуется как раз-таки RS232, и ещё есть дополнительные выводы, которые сейчас используются очень редко.

Но это не страшно - все друг друга понимают так или иначе.

Утащим из википедии картиночку:



Пока не идёт передача, на выходе должна сидеть логическая единица. Затем мы посылаем байт за байтом на заранее оговоренной скорости передачи. Первым идёт стартовый бит - нолик. За ним - 8 бит данных. Есть варианты с другим количеством бит (5, 6, 7), но сейчас они используются крайне редко. После бит данных может идти бит чётности (паритета, parity bit), а может и не идти - как договоримся. Причём, если этот бит присутствует, он может работать четырьмя разными способами:
- дополнять посылку до чётной (even),
- дополнять посылку до нечётной (odd),
- всегда равняться единице (mark)
- всегда равняться нулю (space)

И наконец, посылка оканчивается одним или двумя стоповыми битами (stop bits), они всегда равны логической единице.

Микросхема cp2102 может работать в любом из форматов и на любой скорости - надо только установить драйвера и выбрать требуемые параметры в настройках появившегося COM-порта.

Поэтому потребительскому устройству обычно позволяется не быть универсальным, а работать на одной-единственной скорости и в выбранном режиме. Разумеется, имея в наличии ПЛИС о 9984 логических ячейках, мы можем реализовать все возможные и невозможные настройки передачи, но не будем.

Реализуем, вероятно, самый популярный вариант передачи - 8 бит данных, 1 стоповый бит, бит чётности (паритета) отсутствует, скорость передачи будет объявлена параметром, который мы сможем менять в широких пределах.

Сначала напишем модуль, передающий 1 байт. Вот его примерный заголовок:

module UARTtransmitter( input clk,
			input [7:0] Data,
			input st,
			output txd,
			output Ready);


clk - это, разумеется, тактовая частота, нас устраивает практически любая.

На вход st должен прийти единичный импульс - сигнал к началу передачи. В этот же такт на вход Data мы должны подать байт, который хотим передавать - уже к следующему такту он "защёлкнется" в сдвиговый регистр, и дальше на вход Data можно подавать что угодно, на работу передатчика это не повлияет.

С txd поступает выходной сигнал.

Когда передача будет окончена, на выход Ready поступит единичный импульс, который известит вышестоящую цепь, что можно "защёлкивать" на вход очередной байт данных или, если мы передали всё, что хотели - дать сигнал об успешном окончании передачи.

Это не вполне "аккуратный" способ - между переданными байтами возникнет "заминка" на 2-3 такта. Возможно, красивее было бы извещать вышестоящую цепь чуть пораньше, чтобы она ещё во время передачи стопового бита подготовила следующий байт, подала бы единичный импульс на st, и мы сразу за стоповым битом, без какой-либо задержки, начали бы передавать стартовый для нового байта данных. На высоких скоростях (256 000 бит/с или 512 000 бит/с) это может стать существенным, и в своё время мы разберёмся, как можно сделать совсем хорошо.

Классический способ описания таких передатчиков - в виде конечного автомата (Finite State Machine, FSM). Мы в явном виде вводим переменную (регистр), описывающую состояние системы, и говорим, что именно нужно делать в каждом из состояний, в том числе - нужно ли оставаться в этом состоянии, или пора перейти в новое.

Вот какой код предлагает товарищ с https://www.nandland.com/goboard/uart-go-board-project-part2.html:

//////////////////////////////////////////////////////////////////////
// File Downloaded from http://www.nandland.com
//////////////////////////////////////////////////////////////////////
// This file contains the UART Transmitter.  This transmitter is able
// to transmit 8 bits of serial data, one start bit, one stop bit,
// and no parity bit.  When transmit is complete o_Tx_done will be
// driven high for one clock cycle.
//
// Set Parameter CLKS_PER_BIT as follows:
// CLKS_PER_BIT = (Frequency of i_Clock)/(Frequency of UART)
// Example: 25 MHz Clock, 115200 baud UART
// (25000000)/(115200) = 217
 
module UART_TX 
  #(parameter CLKS_PER_BIT = 217)
  (
   input       i_Clock,
   input       i_TX_DV,
   input [7:0] i_TX_Byte, 
   output      o_TX_Active,
   output reg  o_TX_Serial,
   output      o_TX_Done
   );
 
  parameter IDLE         = 3'b000;
  parameter TX_START_BIT = 3'b001;
  parameter TX_DATA_BITS = 3'b010;
  parameter TX_STOP_BIT  = 3'b011;
  parameter CLEANUP      = 3'b100;
  
  reg [2:0] r_SM_Main     = 0;
  reg [7:0] r_Clock_Count = 0;
  reg [2:0] r_Bit_Index   = 0;
  reg [7:0] r_TX_Data     = 0;
  reg       r_TX_Done     = 0;
  reg       r_TX_Active   = 0;
    
  always @(posedge i_Clock)
  begin
      
    case (r_SM_Main)
      IDLE :
        begin
          o_TX_Serial   <= 1'b1;         // Drive Line High for Idle
          r_TX_Done     <= 1'b0;
          r_Clock_Count <= 0;
          r_Bit_Index   <= 0;
          
          if (i_TX_DV == 1'b1)
          begin
            r_TX_Active <= 1'b1;
            r_TX_Data   <= i_TX_Byte;
            r_SM_Main   <= TX_START_BIT;
          end
          else
            r_SM_Main <= IDLE;
        end // case: IDLE
      
      
      // Send out Start Bit. Start bit = 0
      TX_START_BIT :
        begin
          o_TX_Serial <= 1'b0;
          
          // Wait CLKS_PER_BIT-1 clock cycles for start bit to finish
          if (r_Clock_Count < CLKS_PER_BIT-1)
          begin
            r_Clock_Count <= r_Clock_Count + 1;
            r_SM_Main     <= TX_START_BIT;
          end
          else
          begin
            r_Clock_Count <= 0;
            r_SM_Main     <= TX_DATA_BITS;
          end
        end // case: TX_START_BIT
      
      
      // Wait CLKS_PER_BIT-1 clock cycles for data bits to finish         
      TX_DATA_BITS :
        begin
          o_TX_Serial <= r_TX_Data[r_Bit_Index];
          
          if (r_Clock_Count < CLKS_PER_BIT-1)
          begin
            r_Clock_Count <= r_Clock_Count + 1;
            r_SM_Main     <= TX_DATA_BITS;
          end
          else
          begin
            r_Clock_Count <= 0;
            
            // Check if we have sent out all bits
            if (r_Bit_Index < 7)
            begin
              r_Bit_Index <= r_Bit_Index + 1;
              r_SM_Main   <= TX_DATA_BITS;
            end
            else
            begin
              r_Bit_Index <= 0;
              r_SM_Main   <= TX_STOP_BIT;
            end
          end 
        end // case: TX_DATA_BITS
      
      
      // Send out Stop bit.  Stop bit = 1
      TX_STOP_BIT :
        begin
          o_TX_Serial <= 1'b1;
          
          // Wait CLKS_PER_BIT-1 clock cycles for Stop bit to finish
          if (r_Clock_Count < CLKS_PER_BIT-1)
          begin
            r_Clock_Count <= r_Clock_Count + 1;
            r_SM_Main     <= TX_STOP_BIT;
          end
          else
          begin
            r_TX_Done     <= 1'b1;
            r_Clock_Count <= 0;
            r_SM_Main     <= CLEANUP;
            r_TX_Active   <= 1'b0;
          end 
        end // case: TX_STOP_BIT
      
      
      // Stay here 1 clock
      CLEANUP :
        begin
          r_TX_Done <= 1'b1;
          r_SM_Main <= IDLE;
        end
      
      
      default :
        r_SM_Main <= IDLE;
      
    endcase
  end
  
  assign o_TX_Active = r_TX_Active;
  assign o_TX_Done   = r_TX_Done;
  
endmodule


Здесь мы видим другие синтаксические конструкции Verilog.

Сразу за объявлением модуля мы видим строчку
#(parameter CLKS_PER_BIT = 217)


Это означает: мы указали не только список аргументов (входные и выходные цепи), но и список параметров, "торчащих наружу". Без такого указания, все parameter будут доступны извне, а так - только CLKS_PER_BIT. Но Quartus II 9.0sp2 не любит такой синтаксис, выдавая гору предупреждений:

Warning (10222): Verilog HDL Parameter Declaration warning at UART_TX.v(26): Parameter Declaration in module "UART_TX" behaves as a Local Parameter Declaration because the module has a Module Parameter Port List

Иными словами, Quartus предлагает для пущей наглядности заменить ключевое слово parameter на localparam, чтобы уже не возникало сомнений, что это такое.

Далее мы видим, что человек придерживается правил именования всех переменных (цепей, регистров): входные начинаются с i_, выходные - с o_, внутренние регистры - с r_. Может пригодиться, хотя как по мне, clk всегда и везде будет входной цепью (ведь тактовая частота поступает в ПЛИС извне!), также есть вполне стандартные названия D для входных данных и Q для выходных, cin и cout для входа и выхода переноса, clr, aclr и sclr для сброса, ce для clock enable и ceo для clock enable - output, и пр. Но где-то можно и применить, а иногда это корпоративный стандарт :)

И наконец, здесь используется конструкция case. Её применение сразу же делает код на verilog похожим на код обычных языков программирования, возникает чувство, что в кои-то веки код выполняется последовательно, строка за строкой, но это не так :) В действительности, синтезатор (компилятор) превращает это в код, подобный тому, который мы применяли ранее: определяется логическая функция для каждого из регистров, и все они ожидают прихода положительного фронта тактовой частоты, чтобы одновременно защёлкнуться!

При синтезе данного модуля начинают твориться чудеса: Quartus понимает, что перед ним конечный автомат, и находит для него более подходящую форму записи. Вместо 3-битного регистра r_SM_MAIN (State Machine - MAIN) создаются однобитные регистры для каждого из возможных состояний, почему-то 7 штук. Об этом можно узнать в окне Compilation Report, в разделе Analysis&Synthesis - State Machines. Потом, правда, от двух из регистров удаётся избавиться, о чём свидетельствует строка

Info: 2 registers lost all their fanouts during netlist optimizations. The first 2 are displayed below.
Info: Register "r_SM_Main~10" lost all its fanouts during netlist optimizations.
Info: Register "r_SM_Main~11" lost all its fanouts during netlist optimizations.


Также мы обнаруживаем, что делитель частоты опять был реализован в виде сумматора (lpm_add_sub).

Всего же данный модуль занимает 51 логическую ячейку, и это неплохой результат! Глядя на этот модуль, ожидаешь куда худшего - он весьма громоздок, занимая 146 строк. Так что по одной логической ячейке на каждые 3 строки :) Компилятор проделал очень серьёзную работу, приблизившись к уровню человека.

И всё-таки, мы можем и лучше.

Вот наш код:
`include "math.v"

module SimpleUARTtransmitter (input clk, input st, input [7:0] Data,
                              output txd, output ready);

	parameter CLKfreq = 80_000_000;
	parameter BAUDrate = 512_000;

	localparam DividerBits = `CLOG2(CLKfreq / BAUDrate);
	localparam Limit = CLKfreq / BAUDrate - 1;
					
	reg [DividerBits - 1 : 0] FreqDivider;
							
	reg [7:0] ShiftReg = 8'b1111_1111;

	`define txB1		4'b0000
	`define txB2		4'b0001
	`define txB3		4'b0010
	`define txB4		4'b0011
	`define txB5		4'b0100
	`define txB6		4'b0101
	`define txB7		4'b0110
	`define txB8		4'b0111
	`define txStop		4'b1000
	`define txIdle 		4'b1001
	`define txStart 	4'b1111

	reg [3:0] State = `txIdle;

        wire isIdle = State[3]&(~State[1])&State[0];

        wire ce = ((FreqDivider&Limit) == Limit);

        assign txd = 	~State[3]? ShiftReg[0] : ~State[2];

        assign ready = State[3] & (~State[2]) & ce;

        always @(posedge clk) begin
		FreqDivider <= isIdle|ce? 1'b0: FreqDivider + 1'b1;
		
		State <= st ? `txStart : ce? State + 1'b1 : State;
	
		ShiftReg <= st?               Data :
			    (~State[3] & ce)? (ShiftReg >> 1'b1) :
                                              ShiftReg;
											
end


По сути, и мы применили конечный автомат, но объединили его со счетчиком битов, причем так расположив состояния, чтобы переход из одного состояния в другое осуществлялся обычным прибавлением единицы, кроме момента запуска, когда придётся перескочить в нужное место. Также мы постарались сделать так, чтобы в комбинаторной логике, определяющей следующее состояние и выходные цепи, хватало 1-2 битов состояния.

Первоначально мы должны сидеть в состоянии `txIdle. Цепь isIdle принимает значение 1, что заставляет счетчик FreqDivider непрерывно "сбрасываться" в ноль. Соответственно, цепь ce, означающая, что прошёл один период UART, будет сохранять нулевое значение - всё молчит.

Когда мы подадим единичный импульс на st, это заставит нас перейти в состояние `txStart. С этого момента запускается "командоаппарат", который последовательно отсчитывает состояния `txStart, затем `txB1, `txB2, ... `txB8 и, наконец, `txStop и `txIdle. Приходя сюда, мы снова застрянем, поскольку цепь isIdle начнёт непрерывно сбрасывать счетчик FreqDivider, и выдача ce прекратится.

Для эффективного формирования ce мы используем хитрость, описанную в части 8 - проверяем не строгое равенство FreqDivider == Limit, а лишь наличие единичек на своих местах, что может здорово нам помочь, когда тактовая частота высокая (80 МГц), а скорость передачи мы выбрали совсем низкую, к примеру, 9600 бит/с.

Дальше посмотрим, что происходит со сдвиговым регистром. Всё достаточно очевидно: при поступлении единичного импульса st мы загружаем данные со входа Data, а при поступлении импульсов ce, при условии, что мы находимся в состояниях `txB1 .. `txB8 (о чём свидетельствует нулевой старший бит) - производим сдвиг данных вправо. Что интересно, в UART принято передавать данные от младших битов к старшим, именно поэтому мы извлекаем значение ShiftReg[0], а затем смещаем все биты вправо.

Ещё раз замечаем, что во всех состояниях, где передаются данные, старший бит state нулевой. Состояния `txStop и `txIdle, когда на выход надо подавать единицу, имеют нулевой предпоследний бит, а состояние `txStart, когда надо подать на вход нолик - единичный предпоследний бит.

Таким образом, комбинаторная логика txd (выхода передатчика) умещается в весьма компактное выражение, для которого достаточно 1 ГФ (LUT):

 assign txd = 	~State[3]? ShiftReg[0] : ~State[2];


И наконец, смотрим, как сформировать единичный импульс окончания работы. Мы отлавливаем достижение состояния `txStop. Поскольку много битовых комбинаций остались незадействованы, нам опять достаточно проверки двух старших бит. Тот импульс ce, что переведёт нас из состояния `txStop и `txIdle, и попадёт на выход Ready.

Запускаем синтез, наслаждаемся, как Quartus находит аж два счетчика lpm_counter - и на делитель частоты, и на конечный автомат, который оказался не сложнее командоаппарата стиральной машины.

Финальный результат - 27 логических ячеек, при том, что регистров используется 20:
- 8 бит - делитель частоты (мы специально выбрали такую скорость передачи, чтобы можно было честно сравнивать наш модуль с чужим, где тоже на делитель отведено 8 бит)
- 4 бита - конечный автомат, меньше никак нельзя, ведь у нас 11 состояний
- 8 бит - сдвиговый регистр, от него никуда не денешься, если мы хотим позволить внешней схеме играться с входом Data, пока мы передаём байт.

Ещё одна логическая ячейка очевидно нужна для формирования выходного сигнала txd, и ещё одна - для сигнала Ready.

Так что мы очень близки к теоретическому пределу.

К сожалению, Quartus преподносит нам свинью.
Казалось бы, оптимизация выражений компилятором и вставка блоков lpm_counter или lpm_add_sub не должна менять поведения кода, он просто обязан работать так, как мы указали! Увы, из этого правила есть исключения.

Как показало моделирование, а затем и работа на ПЛИС, регистр state не инициализируется значением `txIdle, как мы просили. Он инициализируется нулём, то есть `txB1 - передача данных начинается сразу...

Чтобы это точно не навредило, мы сделали небольшой финт ушами - инициализировали сдвиговый регистр ShiftReg единицами, поэтому передача действительно начнётся, но сплошных единиц. Затем последует стоповый бит - тоже единица, и, наконец, мы выйдем в режим ожидания. Таким образом, никакого мусора на выходе не будет. Лишь одно плохо - единичный импульс Ready всё-таки будет выдан, причём с приличной задержкой. Важно, чтобы вышестоящая цепь восприняла его адекватно.

И ещё одна особенность: данный модуль не шибко параноидальный, он верит людям, не ждёт от них подлости в виде невовремя пришедшего импульса st. По логике работы, он должен приходить только в режиме ожидания, а в течение передачи данных - игнорироваться. Здесь же он перезапустит передачу. Если мы хотим оградить себя от подобных фокусов, код надо самую малость усложнить. Модифицированный код внутри always-блока выглядит так:
  FreqDivider <= isIdle|ce? 1'b0: FreqDivider + 1'b1;
		
  State <= (st&isIdle) ? `txStart : ce? State + 1'b1 : State;
	
  ShiftReg <= (st&isIdle)?       Data :
	      (~State[3] & ce)?	(ShiftReg >> 1'b1) :
			         ShiftReg;

Одной логической ячейкой становится больше.

Чтобы получше понимать, что творится внутри этого драндулета, вынесем регистры наружу. Вот как оно выглядит:


Мы видим "фальстарт", заканчивающийся выходом в режим ожидания и выдачей импульса Ready. Затем мы подаём импульс st, одновременно подавая число 42 в качестве данных.

Тут уже всё срабатывает чётко - из состояния 9 (`txIdle) мы переходим в состояние 15 (`txStart), на выход подаётся нолик. Затем - идёт передача 8 бит данных, младшим битом вперёд. Наконец, идёт стоповый бит, а после этого - выдаётся импульс Ready, и мы повторно застреваем в режиме ожидания.

Осталось проверить работу этой штуки на ПЛИС.

Создаём схемотехнический символ для нашего SimpleUARTtransmitter, кидаем его на схему верхнего уровня. Перед ним размещаем блок lpm_constant (libraries - megafunctions - gates - lpm_constant), задаём количество бит - 8, и шестнадцатиричное значение - 42. По принципиальной схеме отладочной платы и таблице соответствия находим номер вывода для передачи данных UART: PIN_191. Не забываем вбить параметр BAUDrate = 9600.



Компилируем и прошиваем в ПЛИС.

Наконец, скачиваем программку Termite (к примеру) и настраиваем COM-порт. Находим в диспетчере задач CP210x USB to UART bridge, обычно это COM3. Выбираем его в Termite, настраиваем скорость передачи 9600 бит/с, число бит - 8, стоповых - 1, четность - none. Открываем порт и торжественно нажимаем на кнопочку. Если всё сделано правильно, у нас появится латинская буква B, а при включённом режиме hex - ещё и число 42.

Не прошло и 7,5 млн. лет. Очень медленно мы продвигаемся, но хочется же не просто надёргать кусков кода и слепить из них нечто фурычащее, а как следует разобраться в синтаксисе и в происходящем "под капотом", сделать не как-нибудь, а близко к оптимуму.


Если кто знает ещё более компактный UART-передатчик - пожалуйста, напишите, с меня пиво и орешки (или тортик).

И провокационный вопрос: а какой из двух рассмотренных модулей больше вам по душе?
Tags: ПЛИС, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Нахождение двух самых отдалённых точек

    Пока компьютер долго и упорно мучал симуляцию, я пытался написать на ассемблере алгоритм захвата на ближней дистанции. А сейчас на этом коде можно…

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Балансируем конвейер QuatCore

    В пятницу у нас всё замечательно сработало на симуляции, первые 16 миллисекунд полёт нормальный. А вот прошить весь проект на ПЛИС и попробовать "в…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 1 comment