• Button Handler Class in Micropython

    08/13/2024 at 16:23 0 comments

    This Micropython class handles a single button and provides two possible events: Pulse and Long press. It also provides debouncing.

    The .event( ) method shall be called at every execution cycle. The constants for debouncing and Long Press thresholds must be proportional to the timing desired. In the example the function is called every 5 ms. Any pulse that lasts less than 50ms is considered as a bounce and hence discarded. If the button is held for more than 500ms the function returns a single LONG press event.

    Finally, if the button is pulsed within a time 50ms< t < 100ms the function returns a PULSE 

    The function can be experimented here :  https://wokwi.com/projects/406121126820955137

    from machine import Pin, 
    import time
    
    
    print("Button Handler Class")
    
    class buttonHandler:
        def __init__(self,pin):
            self.pin = Pin (pin, Pin.IN, Pin.PULL_UP)
            self.tOn = 0
            self.tmp = 0
            self.evnt = None 
        
        def event(self):
            if self.pin():
                if (self.tOn <10):    # does the key get pressed for at least 10 scans ?
                    self.tOn=0        # no, it was just a bounce, ignore
                    return None                
                else:    
                    self.tmp=self.tOn  # yes, save amount of scans 
                    self.tOn = 0       # and reset counter           
                    
                    if (self.tmp < 100): # does the key get pressed for less than 100 scans ?
                        return 1         # yes, it was a pulse
            else:
                if (self.tOn < 500):
                    self.tOn = self.tOn + 1  # restrict max value, just in case
                    if self.tOn == 100:
                        return 2 # return long only once
                return None
    
    
    b = buttonHandler (12)   # instantiate and init pin
    
    
    while 1:
        bte = b.event()
        if (bte):
            if (bte==1):
                print ("short")
            elif (bte==2):
                print ("long")    
    
                
        time.sleep_ms(5)

  • Normalizing LDR reading

    07/01/2023 at 04:12 0 comments

    An LDR's resistance, RLDR can vary from about 100Ω in the sun light, to over 10MΩ in absolute darkness (source).

    For practical uses we can consider one order of magnitude above and below the extremes, which in numbers means a resistance variation range from 1KΩ to 1MΩ.

    With that in mind it is possible to use the LDR along a voltage divider circuit and calculate the values of the resistors so the output voltage will match a desired range as long as such range is reasonably within the voltage rails, for instance, In a recent project I needed to use the LDR reading from an 8 bit ADC to drive a 5 bit soft PWM, then a range of 128 counts could be easily shifted right to get me the values from 0 to 31 (and avoid a floating point division math).

    We have then 2 unknown variables, R1 and R2 and two known resistance values and their expected output:

    • LDR in dark, R = 1MΩ, Vout = Vdark
    • LDR in brightness, R = 1kΩ, Vout = Vbright

    It is reasonable to consider that Vdark and Vbright will be symmetrical around Vcc/2, and to perform the calculation of the resistors we can normalize the voltage value to the resolution of the ADC (e.g. use a value of 256 in the math instead of 5V).

    Rewriting the terms, considering the half of the ADC scale is 128 and half of the desired range is 64:

    • LDR in dark, R = 1MΩ, ADCount = 128 + 64 = 192
    • LDR in brightness, R = 1kΩ, ADCount = 128 -64 = 64

    That can be solved using the voltage divider equation:

    • ADCcount = ADCrange * R2 / ( R2 + R1//Rldr )

    Along with the parallel resistance equation

    • 1/Req = (1 / Rldr) + (1 / R1)

    Combining both:

    • ADCCount = ADCrange * R2 / { R2 + [ 1/ ( 1/Rldr + 1/R1 ) ]  }

    There are several ways to perform the math, including Wolfram Alpha, and the equation to find the resistances can be calculated using the following string

    (64+128)=256*(r2)/(r2+(1/(1/1000+1/r1))); (128-64)=256*(r2)/(r2+(1/(1/1000000+1/r1)))

    The calculated values will hardly be an exact value, but the E24 series can provide values close enough to get the desired counts from the ADC

    ResistorCalculated Value
    Closest E24 Value
    R1~8072Ω8K2 Ω
    R2~2669Ω2K7 Ω

    It is possible to use the Falstad online simulator to check how good the picked values are, and of course to fiddle with the values. To save time it is possible to use the link below the access the simulation.

    https://tinyurl.com/2db6tud4

    INotice that the Vcc voltage was set to 256 to make it easy to check the ADC count values from the LDR in brightness (left) and in darkness (right). The difference between readings is shown in the middle. Also notice the E24 series values provided good results (no need to use precision resistors)

    Epilogue:

    if the LDR is exposed under extreme conditions the resistance values canreach the maximum values and the voltage range will differ from the calculated, therefore it is necessary to limit the range in software.  It will also be necessary to subtract the value from the minimum expected value to have a light reading value starting from 0 (zero).

    #define VMAX 192
    #define VMIN 64
    ...
    tempValue= adcRead( LDR_CHANNEL);
    if (tempValue > VMAX) lightValue = VMAX;
    if (tempValue < VMIN) lightValue = VMIN;
    lightOutput = tempValue - VMIN;
    ...

    In my case specifically all that's left to do to call it a day is to shift right the light output value to have the Toff within the 0-32 range.

    tOFF = lightOutput >> 2; 

  • Ninja Tap patch for Rock'N'Roller (MSX1)

    03/17/2023 at 04:33 0 comments

    After a suggestion from [gdx] from MRC I took a look at the Rock'N'Roller (MSX1) and started to work on a patch for Ninja/Shinobi Tap multi controller adapter.

    First step was to disassemble the game and figure out how the game deals with the controls. There are several calls to 3 of the BIOS routines that read the keyboard, the joysticks and the trigger buttons (from the joysticks) and decided that the best strategy was to emulate such BIOS routines and return correspondent values based on the buttons pressed on the controllers plugged in the multitap adapter.

    Next step was to figure out a position in RAM out of the game execution to place the patch code -> address 0xdf00 to 0xe300 seemed nice

    Following I have created the alternate routines:

    • ALT_SNSMAT
    • ALT_GTSTCK
    • ALT_GTTRIG

    The alternate routines fall back to the original BIOS call when the Ninja/Shinobi tap is not detected (a flag is reset) . for instance...

    ALT_GTSTCK:
        ld b,a                  ; save row number
        ld a,(ninjaDetectFlag)  ; test for ninja presence
        and a                   ;
        ld a,b                  ; restore row number
        jp z,0d5h ; (GTSTCK)    ; if not present continue with BIOS routine
        ;
        ; read and return STICK value
    ...

    Next step was to generate a list of addresses where the BIOS calls are called and write a quick and dirt and code to replace such calls with the alternate routines created. The +1 is the offset for the instruction code CD (call) at the addresses that call the bios routines.

    ; Patch Original game code 
        ld hl, ALT_SNSMAT  ; patch calls to 0141h (SNSMAT)
        ld (09648h+1),hl 
        ld (0966bh+1),hl 
        ld (09676h+1),hl 
        ld (09685h+1),hl 
        ld (09b7ch+1),hl 
    ...


    Then I have created a function that will be called in the game main loop to scan the joystick ports and detect the presence of a multitap adapter om port A. If an adapter is detected, a flag is marked and the four taps are read and converted as a mirror of a keyboard matrix and joystick directionals and triggers.

    NINJAPATCH:
    	; check for presence of Tap
    	call CHECKTAPS ; c = 0  0  0  0  a1 a0 b1 b0
    	cpl            ;  c = 1  1  1  1 /a1 /a0 /b1 /b0
    	and 08h        ; isolate bit a1 ->  a = 0 0 0 0 /a1 0 0 0
    	ld (ninjaDetectFlag),a  ; /a1 should be 1 if a ninja or shinobi is detected
    	ret z
    	
    ; scan taps
    ;   ld de,0fa7ah  ;  Tap connected to joy port 2
       ld de,0ba3ah  ;   Tap connected to joy port 1
       ld hl,ntapData
       call GETNIN ; read TAPS on port 1	
       
    ; process taps *************************************
    ...
    

     

    Button Mapping

    Tap 1 is mapped as :

    • Up, Down, Left, Right -> Ditto
    • Trigger A - Z
    • Trigger B - X
    • Select  - SEL
    • Start - Escape

    That was done to allow full access and control of Options menu from the game, but it is still possible to access and control the Optiosn menu using the MSX keyboard.

    Tap 2 is mapped as the QSCS (Q, S, Control, Shift) controls

    Tap 3 is mapped as Joystick 1

    Tap 4 is mapped as Joystick 2


    To be continued...