A better learning project with a better board? - Page 1
Forum section: electronics / general

author: paulca
author_rank: Super Contributor
timestamp: July 21, 2025, 10:05:04 am
content: So I got my UPDuino3.1 through the post. Shipping was a fortune, but having a decent amount of IO and apio/yosys tooling was worth it.So, I started yesterday with the goal of creating a Z80 mode 2 "native" IO device. I flit back and forth on whether to do SPI or UART first, but either would do. I have a UART Tx module with a FIFO I made, so I might as well start with it.First practical experiment was just making the FPGA have 4 read registers.Gate the address and signals: If our device and "Read" then latch the contents of the selected read register on the debug LEDs and increment the value ONCE. The LEDs are literally across the io pins for the "will be" databus. I even got the SB-IO primitives working as tristate IO pins.Then I wrote an Ardruino Mega test puppet so I could see the LEDs. After fixing a few bugs, removing the delays and giving it all the Ardunio mega has and ... it looks like a row of 8 barely lit LEDs as expected. Probably barely hitting it at 20khz though.Not exactly a test, but validation enough to proceed.There are still a lot of grey areas in my understanding of FPGAs. Like the exact timing behaviour of unblocking assignments.Example. If I had 2 always blocks like (pigeon / scrap code);On PosEdge CLK:internal_reg <= external_wire;old_reg <= internal_reg;reg_rising <= internal_reg & !old_reg;endOn PosEdge CLK:if( reg_rising )some_other_reg <= internal_reg +1;endOn "external_wire" changing state, does it take 1+synctime clocks or 2+synctime clocks for some_other_reg to change?Thinking out loud. When CLK rises the upper block will "see" the values BEFORE the clock rose and the effects will be immediately (ideal) visible after the clock has risen. So this takes (sync time), ie, how ever long "ext_register" waited in a changed state for our rising CLK.The lower block will be unaware of this, as it will see "reg_rising" from before the CLK rise. It will not see "reg_rising" until the next rising edge. (Ideally) instantly some_other_reg will change.So that's 1 clock + domain sync time (max 1 CLK) + "end" propagation delays.Am I close?

author: booscrawl
author_rank: Contributor
timestamp: July 23, 2025, 10:10:41 pm
content: did you try simulating this withhttps://edaplayground.com/?

author: xvr
author_rank: Frequent Contributor
timestamp: July 24, 2025, 10:40:53 am
content: QuoteNoIt can be assumed (in general) that when a signal has changed its state (on a clock), any other signals it affected will change their state on the NEXT clock (not before)So:internal_reg <= external_wire;= 1 clockreg_rising <= internal_reg & !old_reg;+ 1 clockif( reg_rising )+ 1 clockSo, up to 3 clock (including resync ofexternal_wirefrom external domain)

author: paulca
author_rank: Super Contributor
timestamp: July 24, 2025, 10:56:15 am
content: Thank you both.A humble reminder that I do have the simulator instead of stupid questions. I just got excited when I got a viable dev board and forgot I had a sim.

author: BrianHG
author_rank: Super Contributor
timestamp: July 24, 2025, 04:22:58 pm
content: Quote from: xvr on July 24, 2025, 10:40:53 amLook at the 1 clock I colored in red. How so is this a 1 clock delay.You cannot be counting 'reg_rising' as you already stated it has a 1 clock delay, the one I colored blue...Perhaps you mean the 'some_other_reg<=...' in the next line of code?Remember, it is the '<=' which waits for the next rising clock.

author: xvr
author_rank: Frequent Contributor
timestamp: July 24, 2025, 05:38:23 pm
content: QuoteYes, it gated byreg_rising, that already has 2 clocks delay (one ininternal_reg <= external_wire;and another inreg_rising <= internal_reg & !old_reg;)So total delay ofsome_other_regis 3 clocks

author: BrianHG
author_rank: Super Contributor
timestamp: July 24, 2025, 05:54:03 pm
content: Quote from: xvr on July 24, 2025, 05:38:23 pmThough your total was correct, you need to be careful when wording this for beginners.In this instance, the 'if' sees it's condition in the brackets after the second clock.The 'if' itself does not add a new clock, but it is the '<=' inside where the 3rd clock delay is attributed to the final 'some_other_reg' output.This could be important if the user wishes to add the place the 'some_other_reg' inside the 'if's conditions like '(some_other_reg && .other_stuff.)' A beginner may accidentally count double here with how you originally wrote your post.

author: paulca
author_rank: Super Contributor
timestamp: July 24, 2025, 07:11:09 pm
content: Theif( nWR_fallen )Was only added so I could see some effect.I am starting with registers for an IO device. 4 read, 4 write. If I implement it as it would be with actual devices behind those registers and something might happen if you write to them that you can then see.As it is, nothing will happen. You will write to registers you can't read back and you will read 0s from the ones you can.It won't test anything.So I added the "single" trigger to add the current databus to the respective read register making the FPGA a really simple totaller.It didn't work quite as intended though BECAUSE that "falling" signal is so late to the game it tries to "effect" when the databus is out of hold time and disappears.While that IS the exact scenario I need to avoid, I didn't calculate my test bench timings properly yet. I think it won't matter as I have much more time in reality for a read or write. An interrupt response is different, but I don't need many clocks to "flip a register" on to the bus for that.Sometimes test code added as a "this might help" turns out to be the most awkward thing.

author: iMo
author_rank: Super Contributor
timestamp: July 24, 2025, 07:40:08 pm
content: Not sure this is a good idea for the naming of the "reg_rising"..Code:On PosEdge CLK:if( reg_rising )Everything in your FPGA should happen on the master clock's rising edge only, thus you never "rising something" in parallel with the rising edge of the clock (except the async signals coming from outside - therefore you need the "synchronizers" at the inputs - as you have described one without naming it above). Also do not think about "propagation delays" inside the FPGA and on what is happening inside the FPGA after the rising edge of the clock. Before the clock's rising edge everything in the FPGA has been "done" (a static situation, no activity inside the FPGA). Simply think inclock's rising edgesonly..

author: paulca
author_rank: Super Contributor
timestamp: July 24, 2025, 08:44:11 pm
content: The "rising" / "falling" registers are there so that when the conditions are active, I only act once, not at 12Mhz constantly.ie. add the written value to the read register of the same index.I want that to happen once, when that write transaction happens.I got it working and fixed some bus contention issues, lazily found with the scope, with the slow puppet rig (AVR MCU).So tomorrow, or Saturday I can test it with a real Z80 bus. if what I think I know about that bus is correct this device should IN() OUT() those registers fine.How much can go wrong? The FPGA factor suggests a lot more than Im used to.If I get bogged down again I have another retreat point. I'd like to do memory paging without having to wire up 15 different 74HC gates. That would be an even simplier project. Just a "Page select" register and remapping 3 address lines and 2 CEs/OEs. Downside with that one is the Z80 side set up is far more painful that sticking another IO device on the bus.

author: BrianHG
author_rank: Super Contributor
timestamp: July 24, 2025, 09:39:54 pm
content: Do get bothered by having trouble. Its part of the learning process.As for a real Z80, I've learned that when beginning with FPGAs, it is easier to create a bunch of combinational logic 'wires' that are named for the bus stated you want to capture, then single posedge trigger on that 'wire's' state.It reads easier.It is easier to debug.And you can worry about the sync stuff only being delayed by 1 FPGA clock instead of the 3 or multilayers you appear to be working with now.Example Z80 bus slave device: (you can use this to make anything, like an extended memory manager, or peripheral device)Code:.....input logic Z80_CLK,input logic [15:0] Z80_ADDR,input logic Z80_M1n,input logic Z80_IORQn,input logic Z80_MREQn,output logic Z80_WAIT,input logic Z80_RDn,input logic Z80_WRn,....// These wires define the Z80 bus operation. The logic states were copied from the Z80 datasheet's waveform.wire z80_op_read_opcode = ~Z80_M1n_r && Z80_IORQn_r && ~Z80_MREQn_r && ~Z80_RDn_r && Z80_WRn_r ; // bus controls for read opcode operationwire z80_op_read_memory = Z80_M1n_r && Z80_IORQn_r && ~Z80_MREQn_r && ~Z80_RDn_r && Z80_WRn_r ; // bus controls for memory RD operationwire z80_op_write_memory = Z80_M1n_r && Z80_IORQn_r && ~Z80_MREQn_r && Z80_RDn_r && ~Z80_WRn_r ; // bus controls for memory WR operationwire z80_op_read_port = Z80_M1n_r && ~Z80_IORQn_r && Z80_MREQn_r && ~Z80_RDn_r && Z80_WRn_r ; // bus controls for IO RD operationwire z80_op_write_port = Z80_M1n_r && ~Z80_IORQn_r && Z80_MREQn_r && Z80_RDn_r && ~Z80_WRn_r ; // bus controls for IO WR operationwire z80_op_nop = Z80_IORQn_r && Z80_MREQn_r ; // Bus condition when Z80 has reached CK0 (T1) in data sheet........reg last_z80_op_read_memory ;always (posedge fpga_clock) begin// This may perform 100's of reads depending on the FPGA and Z80 clock.if (z80_op_read_memory) begin...do something....end// This will perform a single-shot read once no matter how fast or slow the Z80 is so long as the FPGA clock is fast enough.last_z80_op_read_memory <= z80_op_read_memory ;if (z80_op_read_memory && ~last_z80_op_read_memory) begin...do something....endendFor example, using the read or write port with the single shot approach would allow you to make a peripheral on a port number which auto-address incremented for a burst read or burst write from the Z80 on a single port.The one-shot will run on the same clock cycle that the Z80 bus enters the matching state, IE, 1 clock delay to activate, where I placed ...do something..., usually the results you find there will be ready 1 clock later.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 10:38:45 am
content: Im a software guy. I think it's normal for me to flounder in WTFs.In reference to your brilliant suggestion. The problem is "my foo" aimed at reducing boiler plate, actually CAUSES it in Verilog. That and my experience trying to get a tri-state Z80 core synthesised for Ice40 left me scarred, battered and bruised.What I have so far:The "D" pins (via a 74LVC8T bidir transceiver) attach to the Lattice SB_IO drivers so I have tristate control over them. I wrap this in a "databus" wiring module. It lets me swap it to a literal tri-state for simulation tooNo registers, just wires. "data_in", "data_out", drive_enable, real io pins.The 74LVC8T DIR and OE wires at in top_level for now, but should be moved into the databus module as it is technically responsible for driving the bus or not and in which direction.A second "always in" shifter can basically be ignored as passive. It brings in the raw signals.nIOREQ, nRD, nRW, A7, A1, A0(for later the rest of the address and M1 and INT, IEI, IEO for interrupt handling).I "clock these in to registers FPGA 12Mhz domain".I think this is where your suggestion to define all "valid" and "defined" bus 'states' upfront as combinational logic off those registers simplifies about 30 lines of my verilog.Also putting the "one shot" behaviour on the actual "behavioural driver" itself, rather than on and arbitrary signal amongst it also sounds far clearer.That "one shot" behaviour is most likely going to be things like setting "FIFO_Write_Enable" and clocking the byte into the peripheral in reg... or whatever.The only thing I have when I get that far is a mickey mouse UART Tx module with FIFO. Surely "Hello World" via FPGA isn't that far away.EDIT:There is a sort of "un-easy partnership" with the D bus.74LVC8T + SBIOThey almost seem like they are doing the same thing. Splitting direction and drive. However, the 74LVC8T cannot control the FPGA pin state or drive state. So both kinda have to exist. They should be very tightly coupled though. Currently the DIR is controlled by the FPGA side "data_out_drive". OE... I pull it low and leave it on, it is in the FPGA, but it's a bit awkward to control, so I left it "ON" for now. In theory if the FPGA is not "out driving", the DIR will be towards the FPGA, if it doesn't want contented on it's side, it has full control to stop it.

author: BrianHG
author_rank: Super Contributor
timestamp: July 25, 2025, 12:40:55 pm
content: In triggering the FIFO case, based on address, you can add to my code:Code:// Place these parameters at top of code so you may configure your IO addressesparameter FIFO_SETTINGS_ADDRESS = 16b'xxxxxxxx10000000'; // THE 'x' are don't care when using these in an A == Bparameter FIFO_RESET_ADDRESS = 16b'xxxxxxxx10000010';parameter FIFO_IO_ADDRESS = 16b'xxxxxxxx10000011';// This will perform a single-shot read once no matter how fast or slow the Z80 is so long as the FPGA clock is fast enough.last_z80_op_read_memory <= z80_op_read_memory ;if (z80_op_read_memory && ~last_z80_op_read_memory) begin // Everything inside this 'IF' will strobe once//...do something....if (Z80_ADDR==FIFO_SETTINGS_ADDRESS ) begin// set UART baud ratebaud_rate <= Z80_DATA;endif (Z80_ADDR==FIFO_RESET_ADDRESS ) begin// reset fifofifo_rd_adr <=0;fifo_wr_adr <=0;endif (Z80_ADDR==FIFO_IO_ADDRESS ) beginfifo_data <= Z80_DATA;fifo_wr_adr <= fifo_wr_adr + 1'b1;endend

author: iMo
author_rank: Super Contributor
timestamp: July 25, 2025, 01:03:48 pm
content: Would it be possible to create a "single cycle Z80"? (like the "single cycle 8051/52" etc).Not single cycle for everything, sure - you know what I mean..

author: BrianHG
author_rank: Super Contributor
timestamp: July 25, 2025, 03:45:51 pm
content: Quote from: iMo on July 25, 2025, 01:03:48 pmWith fair ease. Though, the bus interface would be different. Calls/Jumps/returns would take 2/3 clock cycles.I do not know how many MHZs you would achieve...On the other hand, make it a fixed 4 clock cycles for everything and run your core with a PLL at 4X.If you want to do it, start a new thread.

author: iMo
author_rank: Super Contributor
timestamp: July 25, 2025, 04:26:57 pm
content: Thanks, just asking on the feasibility. I have here some Dallas 8052 single cyclers (33MHz clock) and in past I made some benchmarks against atmega328p and clock2clock the atmega was 2x faster. Those 80xx/Z80 archs are not much performant these days, so perhaps as educational exercise for people interested (if any)..The 4 cycles for everything is a good idea.Btw, some 15y (??) back I played the Manic Miner on an fpga based ZX Spectrum - 8bit guys here were building the Altera based Speccy kits.. It had a switch for fast and normal

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 04:56:39 pm
content: Quote from: BrianHG on July 25, 2025, 12:40:55 pmThank you, but for now I am staying on the IO bus only. I haven't even looked at address mapped IO, though I know it was common in some Z80 systems.It's adaptable though and that forces me to absorb, understand and apply. The only difference is, the triggers aside, my address bus is 8 bits. 4 device, 4 register because... that's what I choose when I designed the address decoders. It seemed the "path of least resistance" at the time.Oh, I see, you are also ignoring the upper 8. I reacted to 16b too soonIt seems you had the whole z80 mapped and wrapped. I have a breadboard with level shifters.... maybe when I get to PCB will I consider the full bus.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 05:02:58 pm
content: On a broader question.When it comes to underlying peripherals, as I am no longer "pussy footing" around tri-state egg shells, I can be a lot more reliant on "wires" over intermediary registers.For example, picking on the UART status register, I can just take the bit I choose and wire it directly to the UART "Tx Enable" line, can't it?I don't, nor should I, attempt to clock things closer or through some form of "software" abstraction/protection layer.If the client writes a 1 to that register it is literally what the UART_Tx sees as "Go with FIFO read and begin transmit!" "wire input tx_enable"

author: BrianHG
author_rank: Super Contributor
timestamp: July 25, 2025, 05:19:31 pm
content: Quote from: paulca on July 25, 2025, 05:02:58 pmDo you mean something like this:Code:if (z80_op_write_memory && ~last_z80_op_write_memory && (Z80_ADDR==UART_IO_ADDRESS )) beginuart_tx_data <= Z80_DATA; // data to be written to UARTuart_tx_ena <= 1'b1 ; // initiate UART transmitend else uart_tx_ena <= 1'b0 ; // make sure UART gets 1 single clock pulse to transmit 1 single byte.With this setup, you guarantee that the uart_tx_data and uart_tx_ena are ready and valid at the same time, then after the if() fails due to the '&& ~last_z80_op_write_memory', the 'else' guarantees that the uart_tx_ena ends on the next clock cycle.Another way to write this: (depending on your coding style, this one may be more flexible)Code:uart_tx_ena <= 1'b0 ; // make the uart_tx_ena's default to 0, or disableif (z80_op_write_memory && ~last_z80_op_write_memory && (Z80_ADDR==UART_IO_ADDRESS )) beginuart_tx_data <= Z80_DATA; // data to be written to UARTuart_tx_ena <= 1'b1 ; // change the default to initiate UART transmit only during the 'if()' conditionendIn this code, the 'uart_tx_ena <= 1'b0' must precede the 'if()' with the enable inside, otherwise it will always be =0.These 2 codes could be used to drive a standard UART core inside the FPGA, or drive outputs to an external UART whose bus clock matches the FPGA's clock you are using internally.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 05:48:37 pm
content: It would be more the case like:Code:reg [7:0] uart_config_register;wire tx_uart_tx_enable;assign tx_uart_tx_enable = uart_config_register; // if 3 is the bit flag for tx_enableThen allow the bus to write that register directly on the bus signals.(the UART_Tx is also 'guarded' on FIFO.isEmpty of course)If that register's job is to set things like enables this or that, stop bits, start bits, polarity, whatever. Where possible can I just "wire" these direct...I suppose... if I'm thinking right... if I would wire them with a jumper IC to IC on a breadboard, then yes. If I need for any reason to "latch", "hold", "delay" .. when I would need a register IC or a bare flip flop, then I clock it in an always block?

author: BrianHG
author_rank: Super Contributor
timestamp: July 26, 2025, 05:34:09 pm
content: Example using a FPGA input to toggle logic:Code:assign tx_ena_output_pin = opt_in_pin_pulse_mode ? reg_tx_ena_1shot : wire_comb_tx_ena_logic ;The opt input pin would select the within your logic either the direct combinational version, or, the registered 1 shot.The other way to select one-clock-shot to continuous:Code:if (z80_op_write_memory && ~last_z80_op_write_memory && (Z80_ADDR==UART_IO_ADDRESS )) beginwould change to:Code:if (z80_op_write_memory && (~last_z80_op_write_memory || opt_in_pin) && (Z80_ADDR==UART_IO_ADDRESS )) beginThe opt in pin here would just prevent the oneshot, making the output continuous without any fancy combinational switch for driving the output pin. This makes a cleaner design with better FMAX.Since you haven't exactly described your external circuit, or what you are trying to interface with or do, anything I say can be taken in many ways.Usually, even with the smallest 2-5$ fpga, we usually try to do all logic inside the FPGA.If you are using a tiny PAL/GAL, then yes, external 74LS/HC logic chips are expected to lighten the load.Even the larger 128 logic cell PLDs can still do a lot on their own, but, you may be using them to trigger a 74HC574 to latch a buss or address state which you don't want inside the PLD due to lack of IO pins.

author: paulca
author_rank: Super Contributor
timestamp: July 29, 2025, 06:04:08 pm
content: So the registers work when hooked up to the Z80. Thanks for the help!I added the raw IO commands to the Z80 "monitor" app. Had this in mindCode:M> o 80 ffM> i 8001M> o 80 ffM> i 8002M> o 80 ffM> i 8003Might be enough time tonight to try the FIFO after dinner.

author: paulca
author_rank: Super Contributor
timestamp: July 29, 2025, 09:32:58 pm
content: I thought, I'll have a stab at it around 9 o'clock and expected to go to bed defeated.and behind the curtain on the Z80 side:Code:M> o 80 00 - disable TxM> o 81 48 - HM> o 81 65 - eM> o 81 6c - lM> o 81 6C - lM> o 81 6F - oM> o 80 01 - enable TxIt has a few rough edges, needs "deburred".Device in person:

author: paulca
author_rank: Super Contributor
timestamp: August 01, 2025, 02:08:47 pm
content: I almost got the UartRx w/ FIFO working.The challenge with it has been the "signalling" and the correct sequencing of the fifo and data register.Possibly a mistake, but I went with "Auto-pop" to data reg. Pop next if available on read.So the first received byte passes straight through the fifo into the RxData register.ByteReady=false, Fifo_empty=true; data_reg = x<byte arrives>ByteReady=true, Fifo_empty=true; data_reg = first byte.<byte arrives>ByteReady=true, Fifo_empty=false; data_reg = first byte.<cpu reads>ByteReady-->false<read triggers pop>ByteReady=true, Fifo_empty=true; data_reg = second byte.<cpu reads>ByteReady-->false<fifo_empty , can't pop>ByteReady=false, Fifo_empty=true; data_reg = xWhen I watch the status register, this is what I see. However the data register is miss aligned. Out by one.> "Hello""elloo"OR> "Hello""Helll"Maybe this weekend I will get a chance to find that bug. The FIFO pop is out by 1 from the read trigger ... somewhere.

author: paulca
author_rank: Super Contributor
timestamp: August 02, 2025, 09:38:09 am
content: I fixed it. Deleted most the "ready" flag handling and just assigned the read register directly from the FIFO. POP on read. The Z80 is far too slow to notice the register change at 24Mhz.

author: BrianHG
author_rank: Super Contributor
timestamp: August 02, 2025, 12:23:28 pm
content: Yup, the FPGA will run circles around the Z80.Z80->FPGA, easy.FPGA->Z80, well, my strategy was to have the FPGA hold at least 1 word. During a read, set and hold that word to the output, then not on the rise trigger of the read, but on the fall trigger / or the end of a read, I would update my next word when ready.This way, usually 0 delay on new Z80 data reads.

author: zapta
author_rank: Super Contributor
timestamp: August 06, 2025, 04:19:04 pm
content: @paulca, do you use a testbench to simulate your design? It makes it much easier to understand what's going on.Also, separating the queue insert/remove operation will result in a simpler design IMO. On the state machine of the transmission side, you can have a state where it waits and pulls the next byte from the queue and in the following states it wait for the next bitrate tick and shifts.