Finalmente, mis manos llegaron al estudio de los FPGA. Pero de alguna manera resulta incorrecto: escribo controladores para hardware para Linux, programo microcontroladores, leo los circuitos (y diseño un poco), necesito crecer más.
Como no me pareció interesante parpadear los LED, decidí hacer algo simple. Es decir, escriba los módulos de receptor y transmisor para UART, combínelos dentro del FPGA (al mismo tiempo entienda cómo usar IP Core) y pruébelo todo en hardware real.
De inmediato digo que no era una tarea hacer un núcleo universal parametrizado de la tarea. Este es solo un proyecto de prueba, sobre el tema de "sentir qué es FPGA y cómo comunicarse con él".
Entonces, comencemos con el receptor. El algoritmo está bastante
bien descrito , por lo que repetiré aquí solo sus puntos principales.
- La frecuencia de muestreo de la señal RX es cuatro veces mayor que la velocidad de transmisión UART requerida.
- Se considera que la condición para el inicio de la recepción es la transición de la señal de entrada RX de un nivel alto a uno bajo, si la recepción no está actualmente en progreso.
- Se considera que la condición para la identificación confiable del bit de inicio es la retención de la señal RX en un estado bajo en el segundo reloj de la frecuencia de muestreo. Al mismo tiempo, prácticamente caemos en el medio del pulso de bits, lo que nos permitirá seguir muestreando los pulsos cada 4 ciclos.
- En caso de un error en los bits de inicio o parada, configure el error de la señal de error. En base a ello, formamos la señal de sincronización rápida, que utilizaremos en el futuro para la sincronización rápida del receptor.
- Después de identificar el bit de inicio, comenzamos la recepción secuencial de bits de datos, comenzando por el más joven. Los datos recibidos se escriben en el registro con un desplazamiento a la derecha en un bit. La condición para el final de la recepción será la detección de un bit de inicio en la posición 0 del registro de desplazamiento.
- La sincronización rápida del receptor consiste en llevarlo a su estado original después de detectar un error cuando la señal RX llega a un nivel alto (esto puede ser una transmisión lógica "1", una transmisión de bit de parada o un estado inactivo de la línea de transmisión).
- La condición para completar con éxito la recepción (valores correctos de los bits de inicio y parada) es la señal completa. A partir de él (cuando la señal rdclk lo sincroniza), se genera una señal de pulso listo, que indica la presencia de datos válidos en el bus rxdata.
Notaré de inmediato que no quería registrar la señal de lectura lista desde el reloj clk (inesperadamente, ¿verdad?), Para no vincular la velocidad del procesamiento de datos posterior con la tasa de cambio UART. Una implementación similar se encuentra en el módulo transmisor (
ver más abajo ). Se realiza un conjunto de prueba de módulos de receptor y transmisor sobre la base de Intel Core Core FIFO de Intel, y con la capacidad de simular diferentes velocidades para el consumidor y el generador de datos. La única limitación es que la frecuencia de reloj del productor y consumidor de los datos no debe ser inferior a la frecuencia de reloj clk.
Módulo receptor (Verilog)// // UART // // rxdata , ready==1 error==0. // ready 1 rdclk. // // : // rx // 2- . , . // 8 - ( 9 ). // 2 - , // . // // . // . '0' , . '1' // idle (. '1') // start- (. '0') // stop- (. '1') module uart_rx( nreset, // (, 0) clk, // UART, .. UART rx, // UART rdclk, // (rxdata, ready) rxdata, // , ready==1 ready, // rxdata ( 1) error, // ( 1) busy, // ( , 1) idle); // ( 1) input wire nreset; // (, 0) input wire clk; // , .. UART input wire rx; // UART input wire rdclk; // output wire[7:0] rxdata; output wire ready; output error; output busy; output idle; // , rdclk reg[2:0] done = 3'b000; // , rdclk //assign ready = (done == 2'b10) ? 1'b1 : 1'b0; assign ready = (done[1] && !done[0]) ? 1'b1 : 1'b0; // reg error = 1'b0; // // error // rx, . wire fastsync = (error && rx); // reg idle = 1'b1; // : // d[9] - , .. == 1 // d[8:1] - // d[0] - , .. == 0 reg[9:0] d = 10'b1xxxxxxxx1; // . 2'b10 wire[1:0] status = { d[9], d[0] }; // . wire complete = (status == 2'b10) ? 1'b1 : 1'b0; // assign rxdata = d[8:1]; // reg busy = 0; // rx reg[1:0] cnt; always @(posedge clk, negedge nreset) begin if(!nreset) begin rxreset(); end else begin if(fastsync) begin rxreset(); end else begin if(busy == 1'b1) begin // -, rx if(cnt == 2'd0) begin // // // (.. ) d <= { rx, d[9:1] }; if(d[1] == 1'b0) begin // , busy <= 1'b0; // error <= (rx == 1'b1) ? 1'b0 : 1'b1; end else begin // if(rx && (d == 10'b1111111111)) begin // busy <= 1'b0; // error <= 1'b1; end else begin // // - - cnt <= 2'd3; end end end else begin // - cnt <= cnt - 2'd1; end end else begin // if(!error) begin // , if(rx == 1'b0) begin // - busy <= 1'b1; // . 1, .. // d[0]==0 d <= 10'b1111111111; // rx 1/2 // 1- - // 2- - (cnt 0) cnt <= 2'd0; // .. , idle <= 1'b0; end else begin // idle <= 1'b1; end end end end end end task rxreset; begin // error <= 1'b0; // (!?) idle <= 1'b1; // busy <= 0; // -, complete d <= 10'b1xxxxxxxx1; end endtask always @(negedge rdclk, negedge nreset) begin if(!nreset) begin done <= 3'b000; end else begin // complete. // ready // complete 0 1 rdclk. done <= { complete, done[2:1] }; end end endmodule
Dado que la señal de entrada RX es asíncrona y (posiblemente) inestable, se conectó un
elemento mayoritario frente al módulo receptor en el
módulo principal . El elemento también está escrito en Verilog, pero su código no tiene sentido aquí. En cambio, una hermosa imagen de un elemento sintetizado.
El esquema sintetizado del elemento mayoritario. La unidad transmisora es aún más simple y, espero, no necesita comentarios adicionales.
Módulo transmisor (asignaciones Verilog bloqueadas y sin bloqueo dentro siempre) // // UART // // : // clk - 4 , // rdclk - txdata, write, fetch. .. clk // txdata - , write/fetch // write - (1=) // fetch - (1=) // tx - UART // idle - (1=, ) // // FIFO dcfifo_component.lpm_showahead = "ON" module uart_tx( nreset, // (, 0) clk, // UART, .. UART rdclk, // txdata, // write, // ( 1) idle, // ( 1) fetch, // , rdclk tx); // UART input wire nreset; // (, 0) input wire clk; // UART input wire rdclk; input wire[7:0] txdata; input wire write; output wire idle; output fetch; output tx; // reg tx = 1'b1; reg fetch = 1'b0; // 4 reg[1:0] div4 = 2'd0; // : reg[3:0] s = 4'd10; // assign idle = (s == 4'd10); // reg[7:0] d; // reg sendstart; // reg canfetch; // , clk reg gotdata = 1'b0; // clock domains reg[2:0] sync = 3'b000; // rdclk write reg wr = 1'b0; // getdata==1 // nextdata // gotdata==1. , // . // gotdata getdata. always @(posedge rdclk, negedge nreset) begin if(!nreset) begin wr <= 1'b0; sync <= 3'b000; // fetch <= 1'b0; end else begin // write wr <= write; // sync <= { gotdata, sync[2:1] }; // gotdata // . // . fetch <= (sync[1] && !sync[0]) ? 1'b1 : 1'b0; end end always @(posedge clk, negedge nreset) begin if(!nreset) begin // div4 <= 2'd0; s <= 4'd10; gotdata <= 1'b0; end else begin // sendstart = 1'b0; // canfetch = wr; if(div4 == 2'd0) begin case(s) 4'd0: begin // sendstart = 1'b1; // , canfetch = 1'b0; end 4'd9: begin // tx <= 1'b1; end 4'd10: begin // idle, end default: begin // , tx <= d[0]; // d <= { 1'b0, d[7:1] }; // , canfetch = 1'b0; end endcase end else begin // div4 <= div4 - 2'd1; if(s < 4'd9) begin // 9 ! canfetch = 1'b0; end end if(canfetch) begin // , d <= txdata; // gotdata <= 1'b1; if(idle ) begin // idle - sendstart = 1'b1; end else begin // s <= 4'd0; end end if(gotdata) begin // , gotdata <= 1'b0; end if(sendstart) begin // tx <= 1'b0; // s <= 4'd1; // div4 <= 2'd3; end else begin if(div4 == 2'd0) begin if(s < 4'd10) begin // s <= s + 4'd1; // div4 <= 2'd3; end end end end end endmodule
La implementación del transmisor anterior provocó una acalorada discusión en los comentarios. Aunque, como resultado, todos parecían estar de acuerdo en que era posible hacer esto, pero con cuidado. Para su tranquilidad, el módulo se reescribió teniendo en cuenta toda la guía mencionada. En mi opinión, no es mucho más complicado que el anterior en términos de percepción humana del algoritmo implementado.
Módulo transmisor (Verilog, ideológicamente correcto) // // UART // // : // clk - 4 , // rdclk - txdata, write, fetch. .. clk // txdata - , write/fetch // write - (1=) // fetch - (1=) // tx - UART // idle - (1=, ) // // FIFO dcfifo_component.lpm_showahead = "ON" module uart_tx( nreset, // (, 0) clk, // UART, .. UART rdclk, // txdata, // write, // ( 1) idle, // ( 1) fetch, // , rdclk tx); // UART input wire nreset; // (, 0) input wire clk; // UART input wire rdclk; input wire[7:0] txdata; input wire write; output wire idle; output fetch; output tx; // reg tx = 1'b1; reg fetch = 1'b0; // 4 reg[1:0] div4 = 2'd0; // : reg[3:0] s = 4'd10; // assign idle = (s == 4'd10); // reg[7:0] d; // reg sendstart; // reg canfetch; // , clk reg gotdata = 1'b0; // clock domains reg[2:0] sync = 3'b000; // rdclk write reg wr = 1'b0; // getdata==1 // nextdata // gotdata==1. , // . // gotdata getdata. always @(posedge rdclk, negedge nreset) begin if(!nreset) begin wr <= 1'b0; sync <= 3'b000; // fetch <= 1'b0; end else begin // write wr <= write; // sync <= { gotdata, sync[2:1] }; // gotdata // . // . fetch <= (sync[1] && !sync[0]) ? 1'b1 : 1'b0; end end // (?) always // sendstart canfetch always @(*) begin // sendstart = 1'b0; if(nreset) begin // canfetch = wr; if(div4 == 2'd0) begin case(s) 4'd0: begin // sendstart = 1'b1; // , canfetch = 1'b0; end 4'd9: begin // end 4'd10: begin // idle, end default: begin // // , canfetch = 1'b0; end endcase end else begin if(s < 4'd9) begin // 9 ! canfetch = 1'b0; end end if(canfetch && idle) begin // idle - sendstart = 1'b1; end end else begin // reset canfetch = 1'b0; end end always @(posedge clk, negedge nreset) begin if(!nreset) begin // div4 <= 2'd0; s <= 4'd10; gotdata <= 1'b0; end else begin if(div4 == 2'd0) begin case(s) 4'd0: begin // sendstart end 4'd9: begin // tx <= 1'b1; end 4'd10: begin // idle, end default: begin // , tx <= d[0]; // d <= { 1'b0, d[7:1] }; end endcase end else begin // div4 <= div4 - 2'd1; end if(canfetch) begin // , d <= txdata; // gotdata <= 1'b1; if(!idle /*s == 4'd10*/) begin // s <= 4'd0; end end else begin // , gotdata <= 1'b0; end if(sendstart) begin // tx <= 1'b0; // s <= 4'd1; // div4 <= 2'd3; end else begin if((div4 == 2'd0) && (s < 4'd10)) begin // s <= s + 4'd1; // div4 <= 2'd3; end end end end endmodule
Para probar el receptor y el transmisor, el módulo principal se escribió en la rodilla. Le pido que no lo jure, conozco los errores de diseño (señal externa asincrónica nreset, falta de reinicio FIFO, etc.). Pero con el propósito de verificar la funcionalidad, no son significativos.
Mi placa de demostración tiene una fuente de señal de 50Mhz. Por lo tanto, en el módulo principal, utilicé PLL, en la salida C0 de la cual formé una frecuencia para trabajar con UART (1.8432Mhz, en realidad 1.843198Mhz) y, por diversión, formé una frecuencia de 300Mhz (salida c1 PLL) para registrar la simulación del circuito de procesamiento de información.
Módulo principal (Verilog) // // .. UART UART, // FPGA, // FIFO IP CORE DCFIFO. // //NB! // SDC- ! // ( if , // ). module uart( input wire clk50mhz, // 50Mhz input wire nreset, // input wire rx, // UART output wire tx, // UART output wire overflow ); // 1.8432Mhz ( 1.843198Mhz) wire clk_1843200; // 1.2288Mhz ( 1.228799Mhz) //wire clk_1228800; // 300Mhz, PLL wire clk300mhz; // UART uart_pll pll50mhz(.inclk0(clk50mhz), .c0(clk_1843200) /*, .c1(clk_1228800)*/, .c1(clk300mhz)); // UART 38400 // (1843200/38400)/4 = 12 ('b1100). // UART 57600 // (1843200/57600)/4 = 8 // UART 115200 // (1843200/115200)/4 = 4 // UART 230400 // (1843200/230400)/4 = 2 // UART 460800 // (1843200/460800)/4 = 1 (.. !) // UART wire uart_baud4; // // .data 1 . // uart_baud4 .clock/ // uart_baud4 .clock uart_osc uart_osc_1(.clock(clk_1843200), .data(5'd2/*5'd4*//*5'd12*/-5'd1), .sload(uart_baud4), .cout(uart_baud4)); //wire uart_baud4 = clk_1843200; // wire rxf; // mfilter mfilter_rx(.clk(clk50mhz /*clk_1843200*/), .in(rx), .out(rxf)); //wire rxf = rx; // wire[7:0] rxdata; wire rxready; wire error; uart_rx uart_rx_1(.nreset(nreset), .clk(uart_baud4), .rx(rxf), .rdclk(clk300mhz /*clk50mhz*/ /*clk_1843200*/), .rxdata(rxdata), .ready(rxready), .error(error)); wire[7:0] txdata; // , , wire txnone; // , wire fetch; wire full; // // uart_baud4 // clk50mhz uart_fifo_rx uart_fifo_rx_1(.data(rxdata), .rdclk(clk300mhz /*clk50mhz*/ /*clk_1843200*/ /*uart_baud4*/), .rdreq(fetch), .wrclk(clk300mhz /*clk50mhz*/ /*clk_1843200*/ /*uart_baud4*/), .wrreq(rxready), .rdempty(txnone), .q(txdata), .wrfull(full)); assign overflow = ~error; uart_tx uart_tx_1(.nreset(nreset), .clk(uart_baud4), .rdclk(clk300mhz /*clk50mhz*/ /*clk_1843200*/), .txdata(txdata), .write(~txnone), .fetch(fetch), .tx(tx)); endmodule
Para las pruebas, se utilizó el generador de tráfico testcom de Zelax. Desafortunadamente, mi adaptador USB / UART se negó a trabajar con velocidades superiores a 230400 BPS, por lo que todas las pruebas se llevaron a cabo a esta velocidad.
Resultado de la prueba con el filtrado de la señal de entrada RX utilizando el elemento mayoritario
Señal Toque Estado de señal

Y aquí se ha eliminado el elemento mayoritario de la entrada.Pero, ¿qué otra cosa podría simular errores arbitrarios al verificar el esquema de sincronización rápida?

Señal Toque Estado de señal

Nota
Lo siento, no tomé cursos de Quartus y no había nadie para hacer preguntas. Sobre lo que me topé y sobre lo que advierto sobre otros FPGA de inicio: asegúrese de crear un archivo SDC en el proyecto y describa las frecuencias de reloj en él. Sí, el proyecto se está construyendo sin él, aunque pueden aparecer advertencias si el sintetizador no puede determinar las características de temporización del reloj. Al principio los ignoré hasta que maté medio día para determinar el problema de por qué en mi módulo receptor al ejecutar el código
if(rx == 1'b0) begin busy <= 1'b1; d <= 10'b1111111111; cnt <= 2'd0; idle <= 1'b0; end else begin
las señales de ocupado e inactivo se configuraron correctamente, pero el contenido del registro d a veces no cambió.
Apéndice: archivo SDC para el proyecto set_time_format -unit ns -decimal_places 3 # 50Mhz, (50/50 duty cycle) create_clock -name {clk50mhz} -period 20.000 -waveform { 0.000 10.000 } ############################################################################## Now that we have created the custom clocks which will be base clocks,# derive_pll_clock is used to calculate all remaining clocks for PLLs derive_pll_clocks -create_base_clocks derive_clock_uncertainty # PLL ? # altpll_component.clk0_divide_by = 15625, # altpll_component.clk0_duty_cycle = 50, # altpll_component.clk0_multiply_by = 576, # altpll_component.clk0_phase_shift = "0", #create_generated_clock -name clk_1843200 -source [get_ports {clk50mhz}] -divide_by 15625 -multiply_by 576 -duty_cycle 50 -phase 0 -offset 0 # baudrate=38400 # 1/4 , .. duty=(1/4)*100=25% #create_generated_clock -name uart_baud4 -source [get_nets {pll50mhz|altpll_component|auto_generated|wire_pll1_clk[0]}] -divide_by 12 -duty_cycle 25 [get_nets {uart_osc_1|LPM_COUNTER_component|auto_generated|counter_reg_bit[0]}] # baudrate=230400 # 1/4 , .. duty=(1/4)*100=50% create_generated_clock -name uart_baud4 -source [get_nets {pll50mhz|altpll_component|auto_generated|wire_pll1_clk[0]}] -divide_by 2 -duty_cycle 25 [get_nets {uart_osc_1|LPM_COUNTER_component|auto_generated|counter_reg_bit[0]}] # baudrate=460800 # 1, PLL, .
| altpll_component | auto_generated | wire_pll1_clk [ set_time_format -unit ns -decimal_places 3 # 50Mhz, (50/50 duty cycle) create_clock -name {clk50mhz} -period 20.000 -waveform { 0.000 10.000 } ############################################################################## Now that we have created the custom clocks which will be base clocks,# derive_pll_clock is used to calculate all remaining clocks for PLLs derive_pll_clocks -create_base_clocks derive_clock_uncertainty # PLL ? # altpll_component.clk0_divide_by = 15625, # altpll_component.clk0_duty_cycle = 50, # altpll_component.clk0_multiply_by = 576, # altpll_component.clk0_phase_shift = "0", #create_generated_clock -name clk_1843200 -source [get_ports {clk50mhz}] -divide_by 15625 -multiply_by 576 -duty_cycle 50 -phase 0 -offset 0 # baudrate=38400 # 1/4 , .. duty=(1/4)*100=25% #create_generated_clock -name uart_baud4 -source [get_nets {pll50mhz|altpll_component|auto_generated|wire_pll1_clk[0]}] -divide_by 12 -duty_cycle 25 [get_nets {uart_osc_1|LPM_COUNTER_component|auto_generated|counter_reg_bit[0]}] # baudrate=230400 # 1/4 , .. duty=(1/4)*100=50% create_generated_clock -name uart_baud4 -source [get_nets {pll50mhz|altpll_component|auto_generated|wire_pll1_clk[0]}] -divide_by 2 -duty_cycle 25 [get_nets {uart_osc_1|LPM_COUNTER_component|auto_generated|counter_reg_bit[0]}] # baudrate=460800 # 1, PLL, .
| counter_reg_bit [ set_time_format -unit ns -decimal_places 3 # 50Mhz, (50/50 duty cycle) create_clock -name {clk50mhz} -period 20.000 -waveform { 0.000 10.000 } ############################################################################## Now that we have created the custom clocks which will be base clocks,# derive_pll_clock is used to calculate all remaining clocks for PLLs derive_pll_clocks -create_base_clocks derive_clock_uncertainty # PLL ? # altpll_component.clk0_divide_by = 15625, # altpll_component.clk0_duty_cycle = 50, # altpll_component.clk0_multiply_by = 576, # altpll_component.clk0_phase_shift = "0", #create_generated_clock -name clk_1843200 -source [get_ports {clk50mhz}] -divide_by 15625 -multiply_by 576 -duty_cycle 50 -phase 0 -offset 0 # baudrate=38400 # 1/4 , .. duty=(1/4)*100=25% #create_generated_clock -name uart_baud4 -source [get_nets {pll50mhz|altpll_component|auto_generated|wire_pll1_clk[0]}] -divide_by 12 -duty_cycle 25 [get_nets {uart_osc_1|LPM_COUNTER_component|auto_generated|counter_reg_bit[0]}] # baudrate=230400 # 1/4 , .. duty=(1/4)*100=50% create_generated_clock -name uart_baud4 -source [get_nets {pll50mhz|altpll_component|auto_generated|wire_pll1_clk[0]}] -divide_by 2 -duty_cycle 25 [get_nets {uart_osc_1|LPM_COUNTER_component|auto_generated|counter_reg_bit[0]}] # baudrate=460800 # 1, PLL, .
¡Muchas gracias a todos los que escribieron comentarios sobre el artículo! De estos, reuní mucha información útil, aunque a veces algo conflictiva. En mi opinión, su valor es mucho mayor que la implementación del algoritmo descrito anteriormente. Y, sin duda, serán útiles para aquellos que también se atreven a meterse en el mundo de los FPGA.Listado de enlaces externos
- Transceptor Asíncrono Universal (Wikipedia)
- Elemento mayoritario (Wikipedia)