Previously used Mega for Lincoln Railway, but security of wired connections is unsatisfactory.

LincolnCode for Model Railway – This project involves and Arduino sketch for a Mega and a VS or Mono program for windows or Linux including Raspbian. Note that connections to this type of board are less reliable and not recommended for many applications. This is however a useful layout for experimenting with communication response times and design of language.

Source now available. This will allow LincolnCode to be adapted to Breakout Boards and final reduced specific functionality PCBs. See below for further planned developments of LincolnCode. LincolnCode is now free software, though highly adapted versions of this initial design may become commercial software. If this occurs, proper acknowledgement would be expected.

Lincoln Utility Board to supersede Mega for model railways and other applications. Lincoln code is planned to be updated. Python scripts on the Raspberry Pi Pico can run as Slave or Master applications. Lincoln Code is one possible driver for a Slave application. The Lincoln Utility Board has many possible uses including education.

An alternative Real Time Clock is available that does not use a battery and for senior students the battery can be glued, if milliseconds are needed.

This project is mainly software and only a small part of it is the sketch. It was seen as desirable to run the exe file across multiple platforms and so Mono was used, along with MonoDevelop. MonoDevelop files are semi compatible with VS. I started in VS (using the GUI editor) and then transitioned to MonoDevelop and was able to update code files, that had been debugged in VS, into MonoDevelop (MD). The main reason why a complete project cannot be copied to MD is that there are settings in the main project file that are incompatible; the C# files transfer just fine, mostly. After this stage Mono was used to directly run VS compiled programs. Subsequently, Wine was used to run standard .exe files. Setting up Wine is not always successful, as programs often fail to respond. We hope that Wine will be improved and versions that function correctly will be mainstream. It should be noted that some releases of Linux are considered to be exceptionally stable, especially if the only software on the computer is necessary for one purpose, hence the desire to evaluate this in the context of controlling machines. The physical computer hardware must also be designed for that purpose, but starting with testing on an ordinary computer and working up to more specialised equipment that is preferably compatible, allows the system to evolve and builds in mechanisms for future low-cost development. One would also hope to specify particular releases of Mono or Wine that are very stable and reliable.

If you look at the code below you can see that this MD program is primarily an interpreter and I have created a language for controlling timed events.

There may be cases where industrial processes can be controlled using this language. However a compiled language is considered to be more secure and that is one possibility for the future.

Currently the communication between the computer and the Arduino is slow to respond, at about a second or so delay. This may be a result of using the USB serial of the Arduino. It may help to use Tx and Rx pins directly, from a USB serial device plugged into the computer, if this is not a signal bottleneck. Even better may be using a true serial port in the computer, however signal conversion is necessary. (There also may currently be a pin clash involving serial pins, however a problem will mainly occur if these pins are written to by the MD program.)

If you run LincolnCode on Ununtu/Linux then there are two or three groups that the user must join: dialout, tty and possibly uucp. This gives access to a serial port. This can be either USB or true RS232.

The instructions for the Arduino are also text based but they are simple and this may be manageable, in terms of creating reliability.

Microtron is open to negotiating and making this (LincolnCode) a student project. The project would likely include the following:

  • Debugging the existing code and improving the handling of syntax errors.
  • Consider alternative hardware configurations that promote reliability. This could involve Arduinos or alternative devices mounted on circuit boards, which contain relays and other electrical control circuitry.
  • Create machine code from a compiler for this LincolnCode language.
  • Improve the LincolnCode language for specifically defined purposes, which may extend beyond model railways.
  • Connecting the Arduino to test circuitry such as LEDs and switches.
  • Assess response times and reliability. Also assess accuracy of the timing of outputs and readings. Result: Using RS232 (w/o USB) with LincolnCode.exe gives a very quick response on the serial monitor when you click a button. The link to the serial monitor in the opposite direction used USB at both ends. This test was carried out using sketchsimple on simplebreakout and an Ubuntu computer with a physical serial port. An RS232 to TTL (3.3V-5.5V) converter was also used. Of course the MCU was an RPi Pico rather than an Arduino Mega where the USB lag was observed from computer to MCU.
  • Possibly increase the time precision if other timing improvements are successful.
  • There may in future be particular breakout boards or MCUs specified and catered for in the program.
  • Evaluate breakout board with Max3232 with DB9 socket incorporated and true serial port in computer, for speed and reliability.
  • Evaluate USB to 3.3V serial converter for speed and reliability.
  • A dynamic user interface could be developed based on pin choices for input, output (analogue or digital) or PWM. AI could possibly be used to help design this, along with compiler/interpreter switches to reflect allowed syntax due to these changes.
  • Controlled intervals between sequential commands are also intended to be an option, as opposed to separating run together commands with a hyphen. A combination may suit also.

The source code for LincolnCode is now available. It is intended for it to be used to control Breakout Boards and final boards in some circumstances. Further development is still intended. Note that resources are handled differently on Linux and Windows, where conversion between “.resx” and “.resources” files can be needed. The project file is also problematic. These comments pertain to MonoDevelop (in conjunction with VirtualStudio on Windows). You will notice that I originally called the service Hartfordshire Railway, but I believe that there are others who use that name, so I found another name that does not seem to clash with others so much: Lincolnshire Railway. The UK has a lot of very nice names for places.

There is a new board from Microtron (shown above) based on the 6ICs BreakOut board that has 3 banks of eight digital relay ready outputs (logic current levels) currently being manufactured. This board can connect to and drive two 8 relay boards.

The next board after that is planned to have a clock mini-board with an interrupt connection. The frequency of this interrupt will be programmable to 1024Hz or 1000Hz. Working with pseudo-milliseconds at 1024 per second could be beneficial as a power of 2 can divide up in useful ways. If any of the other breakout boards are required with this feature, these can be released after a straight forward edit and manufacture (2-3 weeks). This option is better than using the built in millis() function, as it runs off of the same clock as the time and date.

This board will be suitable for the redevelopment of LincolnCode, to a higher standard of reliability. However, two of these banks use a double row of header pins, where the side by side pins are connected to each other and a 24 Way IDC Line Socket can plug in. This includes 12V supply either from the RS232 cable or from an external power adapter. The purpose of this 12V is solely to drive the relay coils. All grounds need to be common, as RS232 is connected to both MCU and External supply. If anyone needs isolated ground then an option to fully disable RS232 or to omit the 12V option, can be provided. This would involve the manufacture of another PCB, but only minor modifications are needed. Header cables will be needed that are capable of carrying the relay coil current.

If you wish to contribute toward the development of this software, via a donation, that would be appreciated. Make it to the following bank account: 06-0313-0252831-00 New Zealand, Microtron NZ.

Microtron has no facilities for dealing with sales tax. If that is required the transaction cannot go ahead.

The new 3 bank output break out board – Lincoln Utility Board

Features the 9IC-3Bank board has include:

  • Millisecond time, which is synchronisable with the real time clock.
  • 3 banks of 8 digital outputs. These can be used to control relays or solid state switching. 2 banks use a double row of header pins, where the pairs of pins are connected. This is suited to the use of a 24 Way IDC Line Socket and ribbon cable connection.
  • 8 DAC outputs.
  • 8 analog inputs.
  • 8 digital inputs.
  • SD card reader (optional)
  • Fram and optional extra removable Fram.
  • User interface, with rotary encoder and screen.
  • Wifi optional.
  • Watchdog capable.

Sketch

The following is an example of Lincoln Code.

The code is meant to be user friendly and fast to get started on. A dictionary of commands would help with that. However now you can look at the source code to understand these commands in detail and you can invent new commands that suit your MCU and board.

It is intended to redevelop Lincoln Code for use with the 9IC-3Bank board. This board should be suitable for use with moderately complex model railways. If it is used in slave mode via RS232, then expansion to a second board is an option.

Also instead of using a controlling computer, the board can run autonomously. However one MCU board can also control another using Rx/Tx communication.

Alternatively the MCU can host a custom website, that allows control of the railway from a phone/tablet.

The DAC outputs can be used to control variable voltage power sources that control the speed on various sections of track.


Microtron is Not GST Registered

This is a draft version of the sketch for this board or earlier. It has milliseconds and has not been fully debugged. Note that the underlying code for the clock has been added to. You may need to make the same edits on newer versions of the clock driver code. Before using this milliseconds code on other boards, check that there is an RTC interrupt connected.

  • MCU units with sensors and datalogging.
  • Break out boards with user interface. Suitable for datalogging and interactive function including stepper motors.
  • Break out board customizations. Sensors, stepper motors, WiFi, SD cards and Fram.
  • Design of circuit boards, including design of circuit boards that are a customization of a break out board pilot project.
  • WiFi and server collection of data.

Cost unpopulated board

Lincoln Utility Board – (9 IC 24 Ch MBOB) – PCB only: $14.00 + $2 Com + postage (Not GST Registered)

Note that many board components are optional and your choices can be tailored to a particular application.

Lincoln Code example

00:00:00 callbackif 4 once-3   // check sensor 4, trigger once
//00:00:00 callbackifgap 4 3
00:00:00 track 0 stop
00:00:00 aux 0 off 
00:00:00 program 0 stop
00:00:00 callbackif 4 end

00:00:00 callbackifnot 3 once-3  // check sensor 3 off, trigger continuously
//00:00:00 callbackifnotgap 3 2
//00:00:00 track 2 forward
00:00:00 sense 2 if  
00:00:00 track 1 forward
00:00:00 sense 2 else
00:00:00 track 2 reverse 
00:00:00 sense 2 end 
00:00:00 callbackifnot 3 end

00:00:00 callbackif 9 repeat  // check sensor 9, trigger continuously
00:00:00 callbackif 9 ignore
00:00:00 track 1 stop
00:00:00 callbackif 9 end

00:00:00 callback 20 repeat  // trigger continuously
00:00:00 callback 20 ignore
00:00:00 track 1 stop
00:00:00 callback 20 end


00:00:05 track 3 forward //test command
00:00:05 track 5 reverse //test command 2
00:00:05 callbackifnotgap 3 1
00:00:15 track 7 stop    //test command 3
00:00:15 callbackifnotcount 3 repeat // reset counter 
00:00:15 callbackgap 20 3
00:00:17 track 0 stop    //stop all tracks
00:00:18 aux 0 off       //turn all auxiliaries off
00:00:18 callback 20 ignore

00:00:19 pwm 0 0
00:00:19 pwm 3 125
00:00:19 callback 0 ignore   // disable only non conditional callbacks
00:00:19 sense 3 if  
00:00:19 track 2 forward
//00:00:19 callbackifnotgap 3 1
00:00:19 sense 3 ignore  
00:00:19 trackstaterev 6 if  
00:00:19 track 2 stop     
00:00:19 trackstaterev 6 end  
00:00:19 sense 3 else
00:00:19 track 2 reverse  
00:00:19 aux 2 stop
00:00:19 sense 3 end  
00:00:19 callbackcount 20 once-2
//00:00:21 callbackcount 21 once-2

00:00:20 analogread 15 if-200-300
00:00:20 track 2 stop   
00:00:20 analogread 15 end
00:00:20 analogread 14 if->-300
00:00:20 track 2 stop   
00:00:20 analogread 14 end

00:00:21 sense 4 ifnot  
00:00:21 track 2 stop   
00:00:21 callbackifnotexp 3 if
00:00:21 track 5 reverse
00:00:21 callbackifnotexp 3 else
00:00:21 track 5 forward
00:00:21 callbackifnotexp 3 end
00:00:21 sense 4 end  
00:00:21 callbackif 3 restore
00:00:21 callback 20 restore
00:00:21 callbackexp 20 ifnot
//00:00:21 callbackexp 20 ifnot
00:00:21 track 2 forward   
00:00:21 callbackexp 20 end

00:00:22 callbackifnot 0 restore
00:00:22 sense 4 ignore
00:00:22 callback 20 execute-force

00:00:23 sense 4 if  
00:00:23 track 2 stop     
00:00:23 sense 4 end  
00:00:23 callback 20 execute

00:00:23 trackstate 6 if  
00:00:23 track 2 stop     
00:00:23 trackstate 6 end  

00:00:23 auxstate 5 if  
00:00:23 track 3 stop     
00:00:23 auxstate 5 end  

00:00:24 sense 4 restore   
    

Python sample code for Fram, DS3231-RTC & MCP4728-DAC

import time
from machine import Pin, I2C
import mcp4728
from ds3231 import DS3231

interrupt_flag=0
def callback(pin):
    global interrupt_flag
    interrupt_flag=time.ticks_ms()

callback(5)
i2c = I2C(id=0, scl=Pin(21), sda=Pin(20))
cs = machine.Pin(0, machine.Pin.OUT)
ds = DS3231(i2c)
pin = Pin(5,Pin.IN,Pin.PULL_UP)

pin.irq(trigger=Pin.IRQ_FALLING, handler=callback) # | Pin.IRQ_RISING

"""
year = 2024 # Can be yyyy or yy format
month = 12
mday = 22
hour = 19 # 24 hour format only
minute = 39
second = 0 # Optional
weekday = 0 # Optional

datetime = (year, month, mday, hour, minute, second, weekday)
ds.datetime(datetime)
"""

print("Found addresses")

i2cscan = i2c.scan()
print(" ".join(hex(n) for n in i2cscan))

dac0=mcp4728.MCP4728(i2c,0x60) 
dac1=mcp4728.MCP4728(i2c,0x61) 
ds.square_wave(freq=ds.FREQ_1) # Does not work with native code: needs modification.

time.sleep_ms(150)

cs.value(0)
time.sleep_ms(15)
dac0.a.value=0
dac0.b.value=100
dac0.c.value=200
dac0.d.value=250


cs.value(1)
time.sleep_ms(15)
dac1.a.value=1000
dac1.b.value=2000
dac1.c.value=3000
dac1.d.value=4000


count=0
while True:
    print(ds.datetime(), str(int(time.ticks_ms() - interrupt_flag)))
    print(count)
    count += 1
    time.sleep_ms(500)

The button cell can be glued and will only be needed for advanced students.

Sample code output

>>> %Run -c $EDITOR_CONTENT

MPY: soft reboot
Found addresses
0x3c 0x50 0x60 0x61 0x68
(2024, 12, 23, 1, 10, 0, 14, 0) 201
0
(2024, 12, 23, 1, 10, 0, 15, 0) 316
1
(2024, 12, 23, 1, 10, 0, 15, 0) 818
2
(2024, 12, 23, 1, 10, 0, 16, 0) 320
3
(2024, 12, 23, 1, 10, 0, 16, 0) 822
4
(2024, 12, 23, 1, 10, 0, 17, 0) 324
5
(2024, 12, 23, 1, 10, 0, 17, 0) 825
6
(2024, 12, 23, 1, 10, 0, 18, 0) 327
7
(2024, 12, 23, 1, 10, 0, 18, 0) 828
8
(2024, 12, 23, 1, 10, 0, 19, 0) 329
9
(2024, 12, 23, 1, 10, 0, 19, 0) 831
10
(2024, 12, 23, 1, 10, 0, 20, 0) 333
11
(2024, 12, 23, 1, 10, 0, 20, 0) 835
12
(2024, 12, 23, 1, 10, 0, 21, 0) 337
13
(2024, 12, 23, 1, 10, 0, 21, 0) 838
14





====================================
Without delay in loop
====================================

594
(2024, 12, 23, 1, 10, 0, 52, 0) 996
595
(2024, 12, 23, 1, 10, 0, 52, 0) 997
596
(2024, 12, 23, 1, 10, 0, 52, 0) 999
597
(2024, 12, 23, 1, 10, 0, 52, 0) 1000
598
(2024, 12, 23, 1, 10, 0, 53, 0) 1
599
(2024, 12, 23, 1, 10, 0, 53, 0) 2
600
(2024, 12, 23, 1, 10, 0, 53, 0) 4
601
(2024, 12, 23, 1, 10, 0, 53, 0) 5
602
(2024, 12, 23, 1, 10, 0, 53, 0) 7
603
(2024, 12, 23, 1, 10, 0, 53, 0) 9
604
(2024, 12, 23, 1, 10, 0, 53, 0) 10




746
(2024, 12, 23, 1, 10, 5, 53, 0) 994
747
(2024, 12, 23, 1, 10, 5, 53, 0) 995
748
(2024, 12, 23, 1, 10, 5, 53, 0) 997
749
(2024, 12, 23, 1, 10, 5, 53, 0) 998
750
(2024, 12, 23, 1, 10, 5, 53, 0) 0
751
(2024, 12, 23, 1, 10, 5, 54, 0) 2
752
(2024, 12, 23, 1, 10, 5, 54, 0) 3
753
(2024, 12, 23, 1, 10, 5, 54, 0) 5
754
(2024, 12, 23, 1, 10, 5, 54, 0) 6




550
(2024, 12, 23, 1, 10, 8, 6, 0) 996
551
(2024, 12, 23, 1, 10, 8, 6, 0) 998
552
(2024, 12, 23, 1, 10, 8, 6, 0) 999
553
(2024, 12, 23, 1, 10, 8, 6, 0) 1
554
(2024, 12, 23, 1, 10, 8, 7, 0) 2
555
(2024, 12, 23, 1, 10, 8, 7, 0) 4
556
(2024, 12, 23, 1, 10, 8, 7, 0) 5

Sample code using 1024Hz interrupt

import time
from machine import Pin, I2C
import mcp4728
from ds3231 import DS3231

i2c = I2C(id=0, scl=Pin(21), sda=Pin(20))
ds = DS3231(i2c)

interrupt_flag=0
last_second=0
def callback(pin):
    global interrupt_flag
    interrupt_flag+=1   # time.ticks_ms()
    global ds
    global last_second
    second = ds.second()
    if second != last_second:
        last_second = second
        interrupt_flag=0

callback(5)
cs = machine.Pin(0, machine.Pin.OUT)
pin = Pin(5,Pin.IN,Pin.PULL_UP)

pin.irq(trigger=Pin.IRQ_FALLING, handler=callback)

"""
year = 2024 # Can be yyyy or yy format
month = 12
mday = 22
hour = 19 # 24 hour format only
minute = 39
second = 0 # Optional
weekday = 0 # Optional

datetime = (year, month, mday, hour, minute, second, weekday)
ds.datetime(datetime)
"""

print("Found addresses")

i2cscan = i2c.scan()
print(" ".join(hex(n) for n in i2cscan))

dac0=mcp4728.MCP4728(i2c,0x60) 
dac1=mcp4728.MCP4728(i2c,0x61) 
ds.square_wave(freq=ds.FREQ_1024) 

time.sleep_ms(150)

cs.value(0)
time.sleep_ms(15)
dac0.a.value=0
dac0.b.value=100
dac0.c.value=200
dac0.d.value=250


cs.value(1)
time.sleep_ms(15)
dac1.a.value=1000
dac1.b.value=2000
dac1.c.value=3000
dac1.d.value=4000


count=0
while True:
    iflag = interrupt_flag   # Important to read 1024Hz msecs before reading clock
    print('{0:4.0f}'.format(count), ds.datetime(), '{0:4.0f} {1:5.1f}'.
          format(iflag, iflag * 1000 / 1024))    
    print(count)
    count += 1
    #time.sleep_ms(500)

=====================================
Output
=====================================
>>> %Run -c $EDITOR_CONTENT

MPY: soft reboot
Found addresses
0x3c 0x50 0x60 0x61 0x68
   0 (2024, 12, 23, 1, 15, 21, 44, 0)  212 207.0
   1 (2024, 12, 23, 1, 15, 21, 44, 0)  728 710.9
   2 (2024, 12, 23, 1, 15, 21, 45, 0)  308 300.8
   3 (2024, 12, 23, 1, 15, 21, 45, 0)  822 802.7
   4 (2024, 12, 23, 1, 15, 21, 46, 0)  312 304.7
   5 (2024, 12, 23, 1, 15, 21, 46, 0)  826 806.6
   6 (2024, 12, 23, 1, 15, 21, 47, 0)  316 308.6
   7 (2024, 12, 23, 1, 15, 21, 47, 0)  830 810.5
   8 (2024, 12, 23, 1, 15, 21, 48, 0)  320 312.5
   9 (2024, 12, 23, 1, 15, 21, 48, 0)  835 815.4
  10 (2024, 12, 23, 1, 15, 21, 49, 0)  326 318.4  


==========================================
Alternative callback method
==========================================
#This approach reduces I2C bus activity.

interrupt_flag=0
last_second=-1
cycles=0

def callback(pin):
    global interrupt_flag
    interrupt_flag+=1   
    global cycles
    if cycles != 2 or interrupt_flag > 1015:
        global ds
        second = ds.second()
        global last_second
        if second != last_second:
            last_second = second
            interrupt_flag=0            
            if cycles < 2:
                cycles += 1

==========================================
Without delay in loop
==========================================

2821 (2024, 12, 23, 1, 15, 20, 10, 0) 1011 987.3
2822 (2024, 12, 23, 1, 15, 20, 10, 0) 1014 990.2
2823 (2024, 12, 23, 1, 15, 20, 10, 0) 1017 993.2
2824 (2024, 12, 23, 1, 15, 20, 10, 0) 1021 997.1
2825 (2024, 12, 23, 1, 15, 20, 11, 0)    2   2.0
2826 (2024, 12, 23, 1, 15, 20, 11, 0)    5   4.9
2827 (2024, 12, 23, 1, 15, 20, 11, 0)    8   7.8
2828 (2024, 12, 23, 1, 15, 20, 11, 0)   11  10.7



2471 (2024, 12, 23, 1, 15, 20, 9, 0) 1011 987.3
2472 (2024, 12, 23, 1, 15, 20, 9, 0) 1014 990.2
2473 (2024, 12, 23, 1, 15, 20, 9, 0) 1017 993.2
2474 (2024, 12, 23, 1, 15, 20, 9, 0) 1021 997.1
2475 (2024, 12, 23, 1, 15, 20, 10, 0)    0   0.0
2476 (2024, 12, 23, 1, 15, 20, 10, 0)    4   3.9
2477 (2024, 12, 23, 1, 15, 20, 10, 0)    7   6.8
2478 (2024, 12, 23, 1, 15, 20, 10, 0)    9   8.8


MCP4728A4

A4 means that the programmed address is 0x61. Arduino IDE can be used to change one of the MCP4728 I2C addresses to 0x60, using SoftI2cMaster and mcp4728_program_address.ino.

The Python driver for the MCP4728 is available here.

//Customise as follows:
#include <Arduino.h>
#include "SlowSoftI2CMaster.h"

// Change to LDAC pin here
#define LDAC_PIN 0

// Define address here (0-7)
#define OLD_ADDRESS 1
#define NEW_ADDRESS 0

#define SDA_PIN 20
#define SCL_PIN 21

SlowSoftI2cMaster i2c;

DS3231

The online micropython library requires a modification to work at 1Hz. Line 226: “if not freq:” needs to exclude zero e.g. if not freq and freq != 0:. Possibly could use “if freq < 0 or freq >= 4”. The case “freq equals zero” needs to invoke the else statement and then the interrupt square wave will work.

MB85RC256V Fram

The Python driver files fram_i2c.py and bdevice.py can be downloaded from here.

from machine import Pin, I2C
from fram_i2c import FRAM
from PiicoDev_SSD1306 import *
import time

i2c = I2C(id=0, scl=Pin(21), sda=Pin(20))
fram = FRAM(i2c)
display = create_PiicoDev_SSD1306(0x3C, 0, None, 20, 21, None)

b3 = bytearray(3)
for index, bval in enumerate(b3):
    b3[index] = index

while True:
    fram.readwrite(0, b3, False)
    fram.readwrite(0, b3, True)
    for index, bval in enumerate(b3):
        b3[index] += 1
    display.fill(0)
    for index, bval in enumerate(b3):
        print(str(bval))
        display.text(str(bval), 30, 15 + index * 15, 1)
    display.show()    
    time.sleep_ms(500)






================================================================================
Program output - also shows on display
================================================================================

>>> %Run -c $EDITOR_CONTENT

MPY: soft reboot
1 chips detected. Total FRAM size 32768bytes.
Using supplied bus, sda, and scl to create machine.I2C() with freq: 400000 Hz
1
2
3
2
3
4
3
4
5
4
5
6
5
6
7
6
7
8
7
8
9

Soldering the Lincoln Utility Board

It is good to start with the two surface mounted devices nearest the Pico chip, TCMD4000-Optocoupler & MB85RC256V-Fram. I use a Yihua 8786D solder station to do this. First, tin the contacts for the device on the Lincoln Utility PCB, with Sn62/Ag2/Pb 0.25mm solder, using the soldering iron. Then, place the chip on the location with pin1 at the top in these cases. Use the hot air attachment at 350 degrees, holding the hot air gun in your left hand and a small fine screw driver in your right hand. When the solder melts, you may need to reposition the chip slightly, with minimum force. Then, push down on the chip and remove the hot air; hold for a moment and then when the solder has solidified; the first stage is complete. Next, look between the legs of the chip for solder bridges; use a magnifier. Add the pairs of jumper pins to connect SCL and SDA for the Fram chip; put jumper shunts onto these pins to connect. Also, test 3.3V and Gnd for continuity; additionally test these points with SCL and SDA for short circuits. If you find a solder bridge, you can add more solder to the problem location in order to melt the solder that is out of reach. Melt the problem location and use a solder sucker to remove the excess solder. This usually fixes the problem without removing and reattaching the chip. Try and find access to each circuit that connects to each pin of the device. Measure continuity from that circuit to top of the physical pin. If a pin is not connected to its pad properly, then use the soldering iron to heat that pin and add a tiny amount of solder to make the connection. If you add too much solder, you may need to clear the excess with the solder sucker.

Next add the female header sockets for the Pico chip and run a test program for the Fram. Note that the pullup resistors are internal and so the locations for these on the PCB may remain empty. You can now add more I2C devices one by one and do an I2C address scan and run a test program after each addition. This avoids having to deal with untraceable bus problems, after assembly without having carried out precautionary testing along the way.

Solder station safety. When soldering or using hot air, aim a small fan at yourself to blow away smoke and fumes. Also, when doing either of these things wear eye protection; this can be a magnifier headset or plastic glasses.

An alternative to tinning the copper pads is to apply solder paste; this should be viscous, thick and not runny. If it it runny then it may have expired. Solder paste should be kept in the fridge and should last about six months before it expires. The advantage of using solder paste is the the components sit flat on the board and can be more easily, correctly positioned. The viscous, sticky paste helps to hold the component in place. When heated with the hot air gun or in a reflow oven, the flux disappears and leaves the surface mounted device connected to the board; solder usually moves off of non-tinned areas. To more accurately apply the correct amount of paste, a stencil may be used, however larger ICs with more widely spaced legs may not need this level of accuracy, if you experiment a bit. In this larger IC case, it is possible to put paste on each individual pad by hand.

Addressable Latch SN74HC259

Here is driver code for the SN74HC259 chip; three of which occur on the Lincoln Utility Board. This code instantiates correctly, but has not been tested on the chips yet.

# Copyright (c) 2024 Stephen Eichler for Microtron Ltd NZ
# Written 24/12/2024
# import latch74hc259
# alatch0=latch74hc259.SN74HC259(8) 
# alatch1=latch74hc259.SN74HC259(1) 
# alatch2=latch74hc259.SN74HC259(26) 
# alatch0.d0.value=1

from machine import Pin

class SN74HC259:

    def __init__(self, enablepin, reverse=False, reset=False, addrb0pin=6, addrb1pin=7, addrb2pin=10, valpin=9):
        self.d0 = Channel(self, 0)
        self.d1 = Channel(self, 1)
        self.d2 = Channel(self, 2)
        self.d3 = Channel(self, 3)
        self.d4 = Channel(self, 4)
        self.d5 = Channel(self, 5)
        self.d6 = Channel(self, 6)
        self.d7 = Channel(self, 7)
        self.epin = Pin(enablepin,Pin.OUT)
        self.epin.value(1)
        self.pin0 = Pin(addrb0pin,Pin.OUT)
        self.pin1 = Pin(addrb1pin,Pin.OUT)
        self.pin2 = Pin(addrb2pin,Pin.OUT)
        self.pinval = Pin(valpin,Pin.OUT)
        self.rev = reverse
        if reset:
            self.d0.value=0
            self.d1.value=0
            self.d2.value=0
            self.d3.value=0
            self.d4.value=0
            self.d5.value=0
            self.d6.value=0
            self.d7.value=0
        

    def _set_lat_addr(self, addr):
        if addr < 0 or addr >= 8:
            addr=0
        if self.rev:
            addr=7-addr
        self.pin0.value(addr & 1)
        self.pin1.value((addr & 2) == 2)
        self.pin2.value((addr & 4) == 4)


    def _set_value(self, channel, val):
        self.epin.value(1)
        self._set_lat_addr(channel.channel_index)
        self.pinval.value(bool(val))
        self.epin.value(0)
        self.epin.value(1)


class Channel:
    """An instance of a single channel for a multi-channel address latch.
    **All available channels are created automatically and should not be created by the user**"""


    def __init__(self, alat_instance, index):
        self._alat = alat_instance
        self.channel_index = index
        self._value=0

    @property
    def value(self):
        """The current value for the channel."""
        return self._value

    @value.setter
    def value(self, value):
        self._value=value
        self._alat._set_value(self, value)  



Digital Demultiplexer SN74HC151N

Here is driver code for the 74HC151 chip; one of which occurs on the Lincoln Utility Board. This code instantiates correctly, but has not been tested on the chip yet.

# Copyright (c) 2024 Stephen Eichler for Microtron Ltd NZ
# Written 24/12/2024
# License: Free to use. Requirement for acknowledgment
# import dmplex74hc151
# dmplex0=dmplex74hc151.SN74HC151() 
# d0 = dmplex0.d0.value

from machine import Pin
from micropython import const
import time

ADDRB0PIN = const(6)
ADDRB1PIN = const(7)
ADDRB2PIN = const(10)
VALYPIN = const(22)


class SN74HC151:

    def __init__(self, valypin=VALYPIN, addrb0pin=ADDRB0PIN,
                 addrb1pin=ADDRB1PIN, addrb2pin=ADDRB2PIN):
        self.ypin = Pin(valypin,Pin.IN)
        self.pin0 = Pin(addrb0pin,Pin.OUT)
        self.pin1 = Pin(addrb1pin,Pin.OUT)
        self.pin2 = Pin(addrb2pin,Pin.OUT)
        self.d0 = Channel(self, 0)
        self.d1 = Channel(self, 1)
        self.d2 = Channel(self, 2)
        self.d3 = Channel(self, 3)
        self.d4 = Channel(self, 4)
        self.d5 = Channel(self, 5)
        self.d6 = Channel(self, 6)
        self.d7 = Channel(self, 7)

    def _set_lat_addr(self, addr):
        if addr < 0 or addr >= 8:
            return
        self.pin0.value(addr & 1)
        self.pin1.value((addr & 2) == 2)
        self.pin2.value((addr & 4) == 4)

    def _read_value(self, channel):
        self._set_lat_addr(channel.channel_index)
        time.sleep_us(2)
        return self.ypin.value()

class Channel:
    """An instance of a single channel for a digital demultiplexer.
    **All available channels are created automatically and should not be created by the user**"""


    def __init__(self, mplex_instance, index):
        self._mplex = mplex_instance
        self.channel_index = index
        self._value=0

    @property
    def value(self):
        """The current value for the channel."""
        self._value=self._mplex._read_value(self)
        return self._value

Analog Demultiplexer SN74HC4051D

Here is driver code for the 74HC4051 chip; one of which occurs on the Lincoln Utility Board. This code instantiates correctly, but has not been tested on the chip yet.

# Copyright (c) 2024 Stephen Eichler for Microtron Ltd NZ
# Written 24/12/2024
# License: Free to use. Requirement for acknowledgment
# import admplex74hc4051
# admplex0=admplex74hc4051.SN74HC4051() 
# a0 = admplex0.d0.value

from machine import Pin, ADC
from micropython import const
import time

ADDRB0PIN = const(6)
ADDRB1PIN = const(7)
ADDRB2PIN = const(10)
VALZPIN = const(27)


class SN74HC4051:

    def __init__(self, valypin=VALZPIN, addrb0pin=ADDRB0PIN,
                 addrb1pin=ADDRB1PIN, addrb2pin=ADDRB2PIN):
        self.zpinadc = ADC(VALZPIN)
        self.pin0 = Pin(addrb0pin,Pin.OUT)
        self.pin1 = Pin(addrb1pin,Pin.OUT)
        self.pin2 = Pin(addrb2pin,Pin.OUT)
        self.d0 = Channel(self, 0)
        self.d1 = Channel(self, 1)
        self.d2 = Channel(self, 2)
        self.d3 = Channel(self, 3)
        self.d4 = Channel(self, 4)
        self.d5 = Channel(self, 5)
        self.d6 = Channel(self, 6)
        self.d7 = Channel(self, 7)

    def _set_lat_addr(self, addr):
        if addr < 0 or addr >= 8:
            return
        self.pin0.value(addr & 1)
        self.pin1.value((addr & 2) == 2)
        self.pin2.value((addr & 4) == 4)

    def _read_value(self, channel):
        self._set_lat_addr(channel.channel_index)
        time.sleep_us(2)
        return self.zpinadc.read_u16() * 3.3 / 65536

class Channel:
    """An instance of a single channel for a analog demultiplexer.
    **All available channels are created automatically and should not be created by the user**"""


    def __init__(self, mplex_instance, index):
        self._mplex = mplex_instance
        self.channel_index = index
        self._value=0

    @property
    def value(self):
        """The current value for the channel."""
        self._value=self._mplex._read_value(self)
        return self._value

Non-I2C Chips soldered onto the PCB, along with Fram for testing I2C bus not compromised

Sample program for addressable latch 74HC259 and analogue read 74HC4051

from machine import Pin, I2C
import sys
import time
from PiicoDev_SSD1306 import *
from fram_i2c import FRAM
import latch74hc259
import admplex74hc4051
import array
import dmplex74hc151

i2c = I2C(id=0, scl=Pin(21), sda=Pin(20))
fram = FRAM(i2c)
display = create_PiicoDev_SSD1306(0x3C, 0, None, 20, 21, None)
alatch0=latch74hc259.SN74HC259(8, True) 
alatch1=latch74hc259.SN74HC259(1) 
alatch2=latch74hc259.SN74HC259(26) 
admplex0=admplex74hc4051.SN74HC4051() 
dmplex0=dmplex74hc151.SN74HC151() 


alatch0.d0.value=1
alatch0.d1.value=1
alatch0.d2.value=1
alatch0.d3.value=1
alatch0.d4.value=0
alatch0.d5.value=0
alatch0.d6.value=0
alatch0.d7.value=0

alatch1.d0.value=1
alatch1.d1.value=1
alatch1.d2.value=0
alatch1.d3.value=0
alatch1.d4.value=1
alatch1.d5.value=1
alatch1.d6.value=0
alatch1.d7.value=0

alatch2.d0.value=1
alatch2.d1.value=0
alatch2.d2.value=1
alatch2.d3.value=0
alatch2.d4.value=1
alatch2.d5.value=0
alatch2.d6.value=1
alatch2.d7.value=0


b3 = bytearray(3)
for index, bval in enumerate(b3):
    b3[index] = index
flarr=array.array('f',[0]*8)
barr=array.array('b',[0]*8)
    
while True:
    flarr[0] = admplex0.d0.value
    flarr[1] = admplex0.d1.value
    flarr[2] = admplex0.d2.value
    flarr[3] = admplex0.d3.value
    flarr[4] = admplex0.d4.value
    flarr[5] = admplex0.d5.value
    flarr[6] = admplex0.d6.value
    flarr[7] = admplex0.d7.value

    barr[0] = dmplex0.d0.value
    barr[1] = dmplex0.d1.value
    barr[2] = dmplex0.d2.value
    barr[3] = dmplex0.d3.value
    barr[4] = dmplex0.d4.value
    barr[5] = dmplex0.d5.value
    barr[6] = dmplex0.d6.value
    barr[7] = dmplex0.d7.value

    outstr="AnalogueIn-"
    for i, fl in enumerate(flarr):
        outstr += ' {0:.3f}{1}'.format(fl, (", " if i < 7 else ""))
    print(outstr)

    boutstr="DigitalIn- "
    for i, bval in enumerate(barr):
        boutstr += str(bval) + (" " if i == 3 else "") # ' {0:.3f}{1}'.format(bval, (", " if i < 7 else ""))
    print(boutstr)

    fram.readwrite(0, b3, False)
    fram.readwrite(0, b3, True)
    for index, bval in enumerate(b3):
        b3[index] += 1
    display.fill(0)
    boutstr=""
    for index, bval in enumerate(b3):
        boutstr += str(bval) + " "
        display.text(str(bval), 30, 15 + index * 15, 1)
    print(boutstr)
    display.show()    
    time.sleep_ms(500)



=========================
Output
=========================

28 29 30 
AnalogueIn- 0.010,  0.153,  0.184,  0.287,  0.204,  0.350,  0.287,  0.415
DigitalIn- 0111 1111
29 30 31 
AnalogueIn- 0.161,  0.286,  0.253,  0.363,  0.245,  0.382,  0.321,  0.454
DigitalIn- 1111 1111

Output summary

This program results in the expected voltages to be measured at the 8 output connections for each of the three 74HC259 chips; either approximately 0V or approximately 3.3V. Analogue read responded to changes in input voltage as expected, as did digital read. The Fram worked as is usual above.

Some PWM options

If Rx/Tx or the SD reader are not being used these pins can be used for PWM. There are three pairs of pins at three frequencies available, from these devices. They are GPIO pins 12, 13, 16, 17 ,18, 19. Micropython does not appear to offer the 8 or 12mA option for these pins. There are hard coding ways to do this available. Here is a PWM demo using the pair of pins, otherwise assigned to Rx/Tx.

from machine import Pin, PWM
import time 


pin12 = machine.Pin(12)
pin12_pwm = PWM(pin12)
pin13 = machine.Pin(13)
pin13_pwm = PWM(pin13)
duty_step = 64 


frequency = 10000
pin12_pwm.freq(frequency)

try:
    while True:
        count= 0
        for duty_cycle in range(0, 65536, duty_step):
            pin12_pwm.duty_u16(duty_cycle)
            pin13_pwm.duty_u16(65535-duty_cycle)
            count+=1
            if count == 64:
                print(str(duty_cycle))
                count=0
            time.sleep_ms(5)
        
        count= 0
        for duty_cycle in range(65536-duty_step, -1, -duty_step):
            pin12_pwm.duty_u16(duty_cycle)
            pin13_pwm.duty_u16(65535-duty_cycle)
            count+=1
            if count == 64:
                print(str(duty_cycle))
                count=0
            time.sleep_ms(5)
        
except KeyboardInterrupt:
    print("Keyboard interrupt")
    pin12_pwm.duty_u16(0)
    print(pin12_pwm)
    pin12_pwm.deinit()
    print(pin13_pwm)
    pin13_pwm.deinit()

=======================================
Here is draft code to set drive current (Not properly verified or tested)
=======================================

from machine import mem32

DRIVE_0=const(0)
DRIVE_1=const(1)
DRIVE_2=const(2)
DRIVE_3=const(3)

def setdrivevalue(pinnum, drivemode=DRIVE_0):
    if pinnum < 0 or pinnum > 28:
        return
    if drivemode < 0 or drivemode > 3:
        return
    drivemodebits = [ 0, 16, 32, 48 ]
    addr = 0x4001c000 
    addr += (pinnum + 1) * 4 
    # bit5bit4 = 00 = 00 = 2mA ;01 = 16 = 4mA ; 10 = 32 = 8mA; 11 = 48 = 12mA
    mode = drivemodebits[drivemode] 
    print(hex(mode), pinnum, hex(addr))
    print(bin(mem32[addr]))    
    machine.mem32[addr] = (machine.mem32[addr] & 0b11001111) | mode
    print(bin(mem32[addr]))

Lincoln LED Bank peripheral

See Lincoln LED bank.