2
votes

I wrote an SPI slave in Verilog. There are some implementations out there, but I am still learning Verilog and digital logic in general, and so I decided I'd try to write it on my own.

My implementation works. But I had to make a change to get it to work, and making the change (I think) puts my implementation at odds with the Motorola SPI spec. I'm wondering: Am I right in thinking this is strange, or do I just not understand how something works?

The SPI signals come in on four wires called SCK, SS, MOSI, and MISO. No surprise there. The FPGA is running at 12MHz and the SPI bus is running at 1MHz. The strategy is to sample bus SPI bus on every FPGA clock posedge and keep track of both the current and last value for SCK and SS so I can detect edges. I also pass each signal through a buffer reg. So the logic is always operating two FPGA clocks behind actual events: On clock 1, I latch the signal in the buffer; on clock 2, I copy it into the reg I use for logic, and on clock 3, I act upon it.

I'm using SPI mode 0. The according to the SPI spec, in mode 0, the slave should sample the MOSI line on the posedge of SCK and then transmit on the negedge of SCK.

So I wrote it just that way:

reg [511:0] data; // I exchange 512-bit messages with the master.

reg [2:0] SCK_buffer = 0;
always @(posedge clock) begin // clock is my FPGA clock
    SCK_buffer <= {SCK_buffer[1:0], SCK};
end 
wire SCK_posedge = SCK_buffer[2:1] == 2'b01;
wire SCK_negedge = SCK_buffer[2:1] == 2'b10;

reg [2:0] SS_buffer = 0;
always @(posedge clock) begin
    SS_buffer <= {SS_buffer[1:0], SS};
end
wire SS_posedge = SS_buffer[2:1] == 2'b01;
wire SS_negedge = SS_buffer[2:1] == 2'b10;
wire SS_active = ~SS_buffer[1];

reg [1:0] MOSI_buffer = 0;
always @(posedge clock) begin
    MOSI_buffer = {MOSI_buffer[0], MOSI};
end
wire MOSI_in = MOSI_buffer[1];     

assign MISO = data[511];

always @(posedge clock) begin 
    if (SS_active) begin
        if (SCK_posedge) begin
            // Clock goes high: capture one bit from MOSI.
            MOSI_capture <= MOSI_in;
        end
        else if (SCK_negedge) begin
            // Shift the captured MOSI bit into the LSB of data and output 
            // the MSB from data.  Note: MISO is a wire that outputs data[511].
            data <= {data[510:0], MOSI_capture};
        end
    end
end

The code should work like this:

  1. When SS goes active (low), data[511] is already output on MISO by virtue of the wire. (No tristate here because I'm the only thing on the bus.)
  2. On posedge of SCK, the slave samples MOSI, and the master gets the data[511] on MISO.
  3. On negedge of SCK, the slave shifts the bit sampled from MOSI into data, causing a new bit to shift into the MISO output position.

When I ran this version, it dropped bits all over the place. Literally half of the 512-bit messages I sent from master to slave wound up corrupted.

I looked at an open-source SPI slave implementation and noticed that it didn't use the negedge of SCK. So I changed my code to this:

always @(posedge clock) begin 
    if (SS_active) begin
        if (SCK_posedge) begin
            // Skip that "capture" business and just shift MOSI right into the
            // data reg.
            data <= {data[510:0], MOSI_in};
        end
    end
end

After making this change, it worked perfectly. Totally bulletproof.

But I'm thinking, huh?

I get that this is okay. MISO gets its new value immediately after the posedge of SCK, and it's still there on the next posedge, when the master samples it. But what was wrong with changing MISO on the negedge? Even though I'm running two FPGA cycles behind the SPI bus due to buffering, I still have 12 FPGA clocks for every one tick of SCK, which means I have six FPGA cycles between SCK negedge and the next posedge. I should be able to get the bit out there on MISO in time, right?

In practice does everyone just (in SPI mode 0) update MISO right after the SCK posedge and not worry about the negedge?

Thanks!

1

1 Answers

2
votes

I think you may be confused about exactly what your master is doing. you say MOSI gets its new value immediately after the posedge of SCK, and it's still there on the next posedge, when the master samples it. Presumably you mean when the slave samples it, but the general idea is that the reader samples on the other clock edge to the one that generated the data, to maximise setup and hold. I think, in your case, the master should be producing data on a falling edge of SCK, not a rising edge.

In any case, the first thing you have to do is to get a datasheet for the master device and find out what it's meant to be doing. Ideally, you should also get a scope on SCLK, MOSI, and SS, and find out what the actual master timing is. Find out when the master changes MOSI, and what the MOSI setup and hold to the SCLK edge (either pos or neg) is.

Your code doesn't actually sample MOSI on an SCK rising edge, and the second version doesn't sample it on a falling edge. It samples somewhere near the edge. The problem is that you've got separate synchronisers on SCK and MOSI (and SS). Generally, this is a recipe for failure, but it might or might not work in your case, depending on the specific timings. Think about it this way. If you're right, and "MOSI gets its new value immediately after the posedge of SCK", then there's a long setup and a short hold on MOSI, and your sampling will fail because, by the time you've seen the high value on SCK (up to 84ns after it actually happened), MOSI has changed and is invalid.

The right way to do this is to sample MOSI (and possibly SS) on a F/F clocked by SCK, and to use a small digital PLL to lock to SCK, so that you know when to read the sampler output. If you understand the timing, and you've got huge setup and hold on everything you're sampling, then you can instead sample SCK as you're currently doing. In this case, you don't need the synchros on MOSI and SS; create an enable signal to sample them when they're known stable.