Menu

Writing to a port pin that is set as input = safe?

2026-03-11
2026-03-22
  • Roger Jönsson

    Roger Jönsson - 2026-03-11

    Writing data fast (via GCbasic) to some port pins simultaneously (not sequential pin update) I need to write a full byte to the port, right? -I can not send data to only some port pins, without writing or rewriting the others(?).
    My question is:
    Can the other port pins be inputs and operated safely while dumping bytes to the port?
    Different chips, different behavior?

     
    • jackjames

      jackjames - 2026-03-15

      I had these needs, and they can be easily managed using bit masks.
      For some lighting effects, I had some pins on a port used as inputs and others as outputs.
      I need to add the NewState value
      on bits 0-5 of LATC,
      bits 6 and 7 are input:

      Update the focus pins:
      ' Focuses 1-6 (bits 0-5) on LATB.0-5, preserving bits 6-7 (strobe)
      LATB = (LATB And %11000000) Or (NewState And %00111111)

      This way, you directly write all the bits you need in a single pass and don't disturb the input bits.

       
      👍
      1
  • Anobium

    Anobium - 2026-03-12

    Writing Whole Bytes vs. Individual Pins on MCU Ports

    When you want multiple pins to change at the same time, the usual approach is to write a full byte (or word) to the port register.
    A single write updates all output pins in that port simultaneously at the hardware level.

    ✔️ Can you update only some pins?

    Not directly.
    If you write to the port register, all output pins in that port are rewritten, even if only one bit changed.

    To safely change only certain pins, you normally:

    1. Read the current port value
    2. Modify the bits you care about
    3. Write the updated byte back

    This preserves the other output bits.


    Using Input Pins While Writing to the Port

    ✔️ Can some pins be inputs while others are outputs?

    Yes.
    Every MCU allows per‑pin direction control

    ✔️ Are input pins affected when you write a byte to the port?

    No — input pins ignore writes to the PORT register.

    • On AVR: writing to a pin configured as input only affects the pull‑up.
    • On PIC: writing to PORT does nothing to input pins; writing to LAT affects only outputs.

    So input pins remain safe while you dump bytes to the port.


    Differences Between AVR and PIC

    MCU Family Notes
    AVR (classic) Writing to PORTx updates all output pins at once. Input pins only toggle pull‑ups.
    AVR DA/DB Have VPORT registers for single‑cycle bit writes. Faster and more flexible.
    PIC 10/12/16 Writing to PORT reads the pin state; writing to LAT ( where present) writes the latch. Bit‑set/clear instructions vary by family.
    PIC 18F Has atomic BSF/BCF on LAT registers; safer for bit manipulation.

    Notes About GCBASIC

    • It will optimize port writes depending on the chip.
    • It may cache the port state, modify the cache, and write it back — often faster than manual bit‑twiddling.
    • Different MCUs produce different ASM because the hardware capabilities differ.

    Summary

    • Yes, you can safely mix input and output pins on the same port.
    • Yes, writing a full byte updates all output pins simultaneously.
    • No, you cannot update only some output pins without rewriting the others — unless the MCU supports special atomic bit‑set/clear instructions (varies by chip).
    • Yes, different chips behave differently, especially AVR vs. PIC.
     
    👍
    1
  • Roger Jönsson

    Roger Jönsson - 2026-03-12

    Thank you so much for the clarification!

     
  • Roger Jönsson

    Roger Jönsson - 2026-03-15

    Trying to solve a problem with timing when setting more than one port.
    This is harder than I thought, since timing is changing depending on what is written to the port before. I thought I had a grasp on it, but then this comes along.
    To demonstrate my problem I coded a symmetric square wave.

    If I set PortC.1 like in the code below, then I have to remove one nop to get the same result.
    What is the reason behind this?

    #Chip 18F57Q84, 1
    #Option Explicit
    
    Do
    PORTC = b'00000000'     
    nop
    nop
    nop
    
    PORTC = b'00000001'  // 3. why is this line affected?
    nop
    nop
    nop
    nop  // 2. <-- problem solved by removing a nop, but why the need?
    
                    // 1.  If I change the below to b'00000010 then I have to 
                    // remove one nop above to get the same timing.
    PORTC = b'00000000'     
    nop
    nop
    nop
    
    PORTC = b'00000011'    
    nop
    nop
    Loop
    
     
  • Anobium

    Anobium - 2026-03-15

    You should be using LAT not PORT ? But, do check the ASM to see if the compiler is already handling as LAT.

     
  • Roger Jönsson

    Roger Jönsson - 2026-03-15

    I have tried LAT and is getting the same result. I just tried it again. Same result.

     
  • Roger Jönsson

    Roger Jönsson - 2026-03-16

    I just realised it could be writing anything else than 0000000 then it takes one cycle longer and I cured it with a nop before it?
    I thought that setting port pins should be one cycle.(?)
    So writing anything else but 0x00 or 0xFF means two cycles?

     
  • Roger Jönsson

    Roger Jönsson - 2026-03-16

    So if set an unused pin, avoid all zeros or ones, I get consistency, it seems. I may lose a cycles here and there, but it gets much easier to handle the results (timing).

    #Chip 18F57Q84, 1
    #Option Explicit
    
    OSCTUNE=8
    
    Do
    
    LATC = b'10000000'     
    nop
    nop
    nop
    
    LATC = b'10000001'     
    nop
    nop
    nop
    
    LATC = b'10000000'     
    nop
    nop
    nop
    
    LATC = b'10000001'    
    nop
    Loop
    
     

    Last edit: Roger Jönsson 2026-03-16
  • Roger Jönsson

    Roger Jönsson - 2026-03-16

    Or store the values in variables, then it is always two cycles.

    It seems that using 0x00 or 0xFF are exceptions that set the ports in one cycle.
    (This may be true for words as well).

     
  • Anobium

    Anobium - 2026-03-16

    This is a reasonable observation based on your experiments with the PIC18F57Q84, but it's likely not a general "exception" for 0x00 / 0xFF specifically—rather, it's tied to how the compiler generates assembly for literal constant values versus variables or more complex expressions.

    And, this is specific to your chip, other microcontrollers may or may not behave the same as each chip family has specific instructiions.

    In GCBASIC when you write something like:

    PORTC = b'00000000'   ' or 0x00
    

    or

    PORTC = b'11111111'   ' or 0xFF
    

    the compiler can often emit a simple single-cycle MOVLW literal + MOVWF LATC (or PORTC if not using LAT properly, but in your case it seems equivalent).

    For many other literal patterns (like b'00000001', b'00000011', or non-power-of-2 values), the compiler may generate slightly different code—sometimes using more instructions or different optimization paths—which can add an extra instruction cycle before the actual write hits the latch.

    When you use a variable instead:

    PORTC = somevar
    

    the compiler loads the value from RAM into W (MOVF somevar, W) then writes it (MOVWF LATC), which reliably takes two cycles (plus any paging/BANKSEL overhead if applicable, though on newer PIC18s with access RAM it's often minimal).

    To achieve consistent timing (especially for tight bit-banged loops or symmetric waveforms), the standard approaches are:

    1. Use variables for the values (as you noted) — this gives predictable 2-cycle writes almost always.

    Example:

    ```basic
    Dim LowVal As Byte : LowVal = 0
    Dim HighVal1 As Byte : HighVal1 = b'00000001'
    Dim HighVal2 As Byte : HighVal2 = b'00000011'

    Do
    LATC = LowVal
    nop
    nop
    nop
    LATC = HighVal1
    nop
    nop
    nop
    nop
    LATC = LowVal
    nop
    nop
    nop
    LATC = HighVal2
    nop
    Loop
    ```

    Adjust nops once and you're done—timing stays rock-solid even if you later change the bit patterns.

    1. Use LATx explicitly (you already tried, but double-check you're compiling with a recent GCBASIC version that properly uses LAT for PIC18 Q-series chips—the compiler should automatically prefer LATx over PORTx for outputs on these devices to avoid read-modify-write issues).

    2. Measure the actual cycles by looking at the generated .ASM file (in the GCBASIC output folder). Search for the MOVWF LATC (or PORTC) instructions around your writes and count the instructions between them. That will show exactly why some literals seem "faster."

    The PIC18 instruction set itself does single-cycle writes to LATx regardless of the value written—there's no silicon-level difference between writing 0x00, 0x01, or 0xFF. The variation you're seeing is purely a compiler code-generation artifact.

    If you share the generated ASM snippet around those PORTC/LATC writes (for the different literal cases), someone on the forum (or here) could pinpoint the exact reason. But storing values in variables is a very practical workaround for stable timing.


    Detailed analysis of the ASM

    Here are the relevant write instructions from your provided ASM (inside the SysDoLoop_S1 loop):

    SysDoLoop_S1
        movlw   128             ; 0x80 = b'10000000'
        movwf   LATC,ACCESS
        nop
        nop
        nop
        movlw   129             ; 0x81 = b'10000001'
        movwf   LATC,ACCESS
        nop
        nop
        nop
        movlw   128
        movwf   LATC,ACCESS
        nop
        nop
        nop
        movlw   129
        movwf   LATC,ACCESS
        nop
        bra     SysDoLoop_S1
    

    Key observations from this generated code:

    • Every single write to LATC uses exactly the same two-instruction sequence:
    • movlw <literal>
    • movwf LATC,ACCESS
    • This is two instruction cycles per write (MOVLW + MOVWF), regardless of whether the value is 128 (0x80) or 129 (0x81).
    • There is no extra instruction, no BANKSEL/MOVLB paging overhead (because LATC is in the access bank), and no skipped/optimized-away MOVLW.
    • The compiler is not generating a single-cycle write for any of these literals.

    This directly contradicts the hypothesis that writes of 0x00 or 0xFF are somehow "single-cycle exceptions" at the hardware level or due to special compiler treatment.

    So what's really causing the observed timing difference?

    Since the ASM is identical (always 2 cycles) for 0x80 vs 0x81 in this sample, the difference you saw in earlier tests must come from one of these:

    1. Different literal values trigger slightly different optimizer paths in GCBASIC
    2. In your actual test program (not this demo), when using LATC = 0 or LATC = 255 (or b'00000000' / b'11111111'), the compiler might occasionally emit optimized code—perhaps reusing a value already in W from a prior instruction, or in rare cases folding the literal into a previous operation. No likely..
    3. For "random" patterns like 0x80/0x81/0x01/0x03/etc., it reliably falls back to the full MOVLW + MOVWF pair.
    4. This would explain why 0x00/0xFF sometimes appeared to need fewer NOPs to balance the waveform.

    5. You were previously using PORTC instead of LATC

    6. On some PIC18s (especially older ones), writing to PORTC can trigger read-modify-write (RMW) behavior if any pin is input — but on Q-series like 18F57Q84 with LATx preferred, GCBASIC should use LAT anyway.

    7. Measurement artifact or loop overhead variation

    8. Small differences in how the branch (bra) aligns or pipeline effects when NOP counts are adjusted.
    9. Oscilloscope triggering or probe capacitance making small cycle differences visible only on certain patterns.

    Bottom line & recommendations

    • The hardware (PIC18F57Q84) always takes one cycle to propagate a write from the LATx register to the pin — the value written makes zero difference.
    • Any perceived 1-cycle vs 2-cycle difference is entirely compiler code-generation artifact — not a silicon feature.
    • In modern GCBASIC (as shown here), even "special" values like 0/255 get the standard 2-cycle MOVLW + MOVWF treatment for these literals.
    • Therefore the safest, most reliable way to get perfectly consistent timing in bit-banged loops is what you already discovered:

    ```basic
    Dim PulseLow As Byte : PulseLow = 0x80
    Dim PulseHigh As Byte : PulseHigh = 0x81

    Do
    LATC = PulseLow
    nop : nop : nop ' adjust once
    LATC = PulseHigh
    nop : nop : nop
    LATC = PulseLow
    nop : nop : nop
    LATC = PulseHigh
    nop
    Loop
    ```

    → Always 2 cycles + your fixed NOP padding. No surprises when you later change bit patterns.

    Variables are cleaner and just as efficient.

    If you have the ASM from your original test program (the one with 0x00 / 0xFF / b'00000001' etc. where you saw the difference), paste the corresponding write section — that would likely show the subtle optimization difference. Otherwise, this pretty much closes the mystery: it's not the chip or the compiler they are doing things as expected.

     
  • Roger Jönsson

    Roger Jönsson - 2026-03-16

    Thank you.
    I think I can see clearly what happens on my oscilloscope. Even when running the PIC at 64MHz I can see a one cycle difference. For even sharper edges, I am running the PIC at 1MHz...
    I had gotten the info from some forums that only setting bits were always one cycle, but clearing was two. From that I tried to compensate with nops. The more I fiddled, the more confused I got. I then turned to AI and got conflicting info (for this particular chip) and it didn't exactly help.
    I think I placed/imagined nops in the wrong place to counteract, since I didn't understand the source of the problem.
    And yes. The mystey is no longer as mysterious.

    Looking at the asm it does clrf when zero and setf when all bits are one.
    The other numbers I've tried does movlw and movwf. Maybe there are other combinations that avoid that (unless the number is the same), but I haven't found them and I am unable to imagine what such a case would be.

    ;PORTC = b'00000000'
        clrf    PORTC,ACCESS
    
    ...
    
    ;PORTC = b'11111111'
    setf    PORTC,ACCESS
    
    ...
    
    ;PORTC = b'00000011'
        movlw   3
        movwf   PORTC,ACCESS
    
     

    Last edit: Roger Jönsson 2026-03-16
  • Anobium

    Anobium - 2026-03-16

    Thanks for sharing the ASM from "Timing mystery writing two pins.gcb"—this nails down exactly what's happening.

    Key part of the generated loop:

    SysDoLoop_S1
        clrf    PORTC,ACCESS          ; ← 1 cycle (for 0x00)
        nop
        nop
        nop
    
        movlw   1
        movwf   PORTC,ACCESS          ; ← 2 cycles (for 0x01)
        nop
        nop
        nop
        nop                           ; ← your extra NOP here
    
        clrf    PORTC,ACCESS          ; ← back to 1 cycle
        nop
        nop
        nop
        nop
    
        movlw   3
        movwf   PORTC,ACCESS          ; ← 2 cycles (for 0x03)
        nop
        nop
        nop
        nop
    
        bra     SysDoLoop_S1
    

    The reason for the timing difference:

    • When you write PORTC = 0 (or b'00000000'), GCBASIC smartly uses the single-instruction clrf PORTC,ACCESS
      → This takes 1 instruction cycle.

    • When you write any other literal value (like b'00000001', b'00000010', b'00000011', etc.), it uses the normal two-instruction sequence:
      movlw <value> + movwf PORTC,ACCESS
      → This takes 2 instruction cycles.

    • On the PIC18F57Q84, the actual pin update from the write is always 1 cycle after the instruction that hits the latch—but because the code to set up the value takes 1 cycle vs. 2 cycles depending on the literal, the time between pin changes varies by 1 cycle unless you pad NOPs differently for each case.

    Important points:

    • This is not a hardware feature of the chip (the PIC doesn't care what value you write—writes to PORTC/LATC are always 1 cycle on Q-series parts).
    • It's purely a GCBASIC code-generation optimization: it knows clrf is shorter/faster for zero, but doesn't have a similar 1-cycle trick for 0xFF or other patterns (no setf emitted).
    • That's why 0x00 feels "faster" (needs fewer NOPs to balance the waveform), and why switching to something like b'00000010' forces you to remove one NOP after the previous write.

    Recommended fix for consistent timing (no surprises when changing patterns):

    Use variables for the port values. This forces GCBASIC to always generate the reliable 2-cycle sequence (movf var,W + movwf PORTC), no matter what bits you put in the variable.

    Example:

    Dim Low    As Byte : Low    = b'00000000'
    Dim Step1  As Byte : Step1  = b'00000001'
    Dim Step3  As Byte : Step3  = b'00000011'
    
    Do
        PORTC = Low
        nop : nop : nop           ' adjust these NOP counts once to get the waveform you want
        PORTC = Step1
        nop : nop : nop : nop
        PORTC = Low
        nop : nop : nop
        PORTC = Step3
        nop : nop : nop
    Loop
    

    → Timing becomes rock-solid and independent of the actual bit patterns. If you later change Step1 to b'00000010' or anything else, you don't have to touch the NOPs again.

    Bonus tip:
    Switch to LATC instead of PORTC in the code above (and in future projects).

    • On PIC18 Q-series parts, Microchip strongly recommends LATx for writes to avoid read-modify-write issues if any pin ever becomes an input.
    • GCBASIC handles LATC the same way (still uses clrf for zero), but it's the safer long-term habit.

    That should resolve the "mystery" completely—it's just the compiler being clever.

     
  • Roger Jönsson

    Roger Jönsson - 2026-03-16

    Yes, I have changed to using LATC, it is just that I opened this code from yesterday and generated the asm.
    I figured that there was some clever short cut somewhere, but could not grasp the logic and since I assumed wrongly, then I ended with one wheel in the ditch and lost traction for a bit.

    Making a long chain of pattern involving more than one pin changing simultaneously and the need for timing being constant, independent of the values written as in this case that cleverness is a disadvantage. Of course, in other cases the clever shortcut will be advantageous. In really time sensitive cases one could flicker the whole port!

     
  • Anobium

    Anobium - 2026-03-16

    The only way would be change the behaviour of the compiler and that is a lot of work. :-(

     
  • Roger Jönsson

    Roger Jönsson - 2026-03-16

    I didn't suggest changing the behaviour of the compiler! -For more than one reason.

    Hopefully a search on the subject will now lead to this thread and clarify things if someone else should wonder.

     
  • Roger Jönsson

    Roger Jönsson - 2026-03-17

    I tested again. As well as the 0x00 one cycle trick, it (as I wrote) does a 0xFF one cycle trick and setf is emitted (the sequence gets shorter by one bit compared to other numbers). It was just that in the asm that you examined there were no 0xFF literal being used in the loop.
    I also tested sending two other literal values after each other to se if this would trigger the cleverness (maybe due to remining in some register). It doesn't. So it seems that 0x00 and 0xFF are the only ones who are handled cleverly (with this chip). -But be observant...

    As said: Use variables as a way around the GC-basic compiler cleverness.
    It made the timing in my code so much easier to handle.

     

    Last edit: Roger Jönsson 2026-03-17

Log in to post a comment.

MongoDB Logo MongoDB