class I2CBus(AbstractBus): """Initialize the I2C hardware driver and represent as bus object.""" type = BusType.I2C frequency = 100000 def start(self): """ """ # Todo: Improve error handling. try: if self.platform_info.vendor == self.platform_info.MICROPYTHON.Vanilla: from machine import I2C self.adapter = I2C(self.number, sda=Pin(int(self.pins['sda'][1:])), scl=Pin(int(self.pins['scl'][1:])), freq=self.frequency) elif self.platform_info.vendor == self.platform_info.MICROPYTHON.Pycom: from machine import I2C self.adapter = I2C(self.number, mode=I2C.MASTER, pins=(self.pins['sda'], self.pins['scl']), baudrate=self.frequency) elif self.platform_info.vendor == self.platform_info.MICROPYTHON.Odroid: from smbus2 import SMBus self.adapter = SMBus(self.number) elif self.platform_info.vendor == self.platform_info.MICROPYTHON.RaspberryPi: import board import busio def i2c_add_bus(busnum, scl, sda): """ Register more I2C buses with Adafruit Blinka. Make Adafruit Blinka learn another I2C bus. Please make sure you define it within /boot/config.txt like:: dtoverlay=i2c-gpio,bus=3,i2c_gpio_delay_us=1,i2c_gpio_sda=26,i2c_gpio_scl=20 """ # Uncache this module, otherwise monkeypatching will fail on subsequent calls. import sys try: del sys.modules['microcontroller.pin'] except: pass # Monkeypatch "board.pin.i2cPorts". i2c_port = (busnum, scl, sda) if i2c_port not in board.pin.i2cPorts: board.pin.i2cPorts += (i2c_port, ) pin_scl = self.pins['scl'] pin_sda = self.pins['sda'] # When I2C port pins are defined as Integers, register them first. if isinstance(pin_scl, int): i2c_add_bus(self.number, pin_scl, pin_sda) SCL = board.pin.Pin(pin_scl) SDA = board.pin.Pin(pin_sda) # When I2C port pins are defined as Strings and start with "board.", # they are probably already Pin aliases of Adafruit Blinka. elif isinstance(pin_scl, str) and pin_scl.startswith('board.'): SCL = eval(pin_scl) SDA = eval(pin_sda) self.adapter = busio.I2C(SCL, SDA) else: raise NotImplementedError( 'I2C bus is not implemented on this platform') self.just_started = True if not self.platform_info.vendor == self.platform_info.MICROPYTHON.Odroid: self.scan_devices() if self.platform_info.vendor == self.platform_info.MICROPYTHON.Odroid: self.devices = self.scan_devices_smbus2() log.info("Scan I2C bus via smbus2 for devices...") log.info("Found {} I2C devices: {}.".format( len(self.devices), self.devices)) self.ready = True except Exception as ex: #log.exc(ex, 'I2C hardware driver failed') raise def scan_devices(self): log.info('Scan I2C with id={} bus for devices...'.format(self.number)) self.devices = self.adapter.scan() # i2c.readfrom(0x76, 5) log.info("Found {} I2C devices: {}".format(len(self.devices), self.devices)) def scan_devices_smbus2(self, start=0x03, end=0x78): try: list = [] for i in range(start, end): val = 1 try: self.adapter.read_byte(i) except OSError as e: val = e.args[0] finally: if val != 5: # No device if val == 1: res = "Available" elif val == 16: res = "Busy" elif val == 110: res = "Timeout" else: res = "Error code: " + str(val) # print(hex(i) + " -> " + res) if res == 'Available': # print(i) list.append(i) return list except Exception as exp: log.exc(exp, 'scan smbus2 failed') def power_on(self): """ Turn on the I2C peripheral after power off. """ # Don't reinitialize device if power on just occurred through initial driver setup. if self.just_started: self.just_started = False return # uPy doesn't have deinit so it doesn't need init if self.platform_info.vendor == self.platform_info.MICROPYTHON.Pycom: from machine import I2C self.adapter.init(mode=I2C.MASTER, baudrate=self.frequency) def power_off(self): """ Turn off the I2C peripheral. https://docs.pycom.io/firmwareapi/pycom/machine/i2c.html """ log.info('Turning off I2C bus {}'.format(self.name)) if self.platform_info.vendor == self.platform_info.MICROPYTHON.Pycom: self.adapter.deinit()
class Host: '''Class to interact with FPGA ''' def __init__(self, platform=None): ''' platform -- object which has gateware settings only passed to controller if virtual test is executed. Needed in lasers.py as each test here has a slightly different TestPlatform ''' # case raspberry if platform is None: self.test = False from gpiozero import LED import spidev from smbus2 import SMBus self.platform = Firestarter(micropython=False) # IC bus used to set power laser self.bus = SMBus(self.platform.ic_dev_nr) # SPI to sent data to scanner self.spi = spidev.SpiDev() self.spi.open(*self.platform.spi_dev) self.spi.mode = 1 self.spi.max_speed_hz = round(1E6) self.chip_select = LED(self.platform.chip_select) # programs TMC2130 self.init_steppers() # stepper motor enable pin self.enable = LED(self.platform.enable_pin) # case micropython: elif upython: self.test = False import machine self.platform = platformmicro(micropython=True) # IC bus self.bus = machine.I2C(self.platform.ic_dev_nr) self.bus.init(machine.I2C.CONTROLLER, adr=self.platform.ic_addr) # SPI # spi port is hispi self.spi = machine.SPI(self.spi_dev, baudrate=round(1E6)) self.chip_select = machine.Pin(self.platform.chip_select) # program TMC2130 # TODO: add TMC2130 library to micropython # self.init_steppers() # stepper motor enable pin self.enable = machine.Pin(self.platform.enable_pin) else: self.platform = platform self.test = True # maximum number of times tried to write to FIFO # if memoery is full self.maxtrials = 10 if self.test else 1E5 self.laser_params = params(self.platform) self._position = np.array([0]*self.platform.motors, dtype='float64') def init_steppers(self): '''configure TMC2130 steppers via SPI Uses teemuatflut CPP library with custom python wrapper https://github.com/hstarmans/TMCStepper ''' import steppers self.motors = [steppers.TMC2130(link_index=i) for i in range(1, 1+self.platform.motors)] steppers.bcm2835_init() for motor in self.motors: motor.begin() motor.toff(5) # ideally should be 0 # on working equipment it is always 2 assert motor.test_connection() == 2 motor.rms_current(600) motor.microsteps(16) motor.en_pwm_mode(True) steppers.bcm2835_close() def build(self, do_program=True, verbose=True): '''builds the FPGA code using nMigen, Yosys, Nextpnr and icepack do_program -- flashes the FPGA chip using fomu-flash, resets aftwards verbose -- prints output of Yosys, Nextpnr and icepack ''' if upython: print("Micropython cannot update binary, using stored one") else: import hexastorm.core as core self.platform = Firestarter() self.platform.laser_var = self.laser_params self.platform.build(core.Dispatcher(self.platform), do_program=do_program, verbose=verbose) if do_program: self.reset() def reset(self): 'restart the FPGA by flipping the reset pin' if upython: import machine reset_pin = machine.Pin(self.platform.reset_pin) else: from gpiozero import LED reset_pin = LED(self.platform.reset_pin) reset_pin.off() sleep(1) reset_pin.on() sleep(1) # a blank needs to be send, Statictest succeeds but # testlaser fails in test_electrical.py # on HX4K this was not needed # is required for the UP5K self.spi_exchange_data([0]*(WORD_BYTES+COMMAND_BYTES)) def get_state(self, data=None): '''retrieves the state of the FPGA as dictionary data: string to decode to state, if None data is retrieved from FPGA dictionary with the following keys parsing: True if commands are executed mem_full: True if memory is full error: True if an error state is reached by any of the submodules x, y, z: state of motor endswitches photodiode_trigger: True if photodiode is triggered during last rotation of prism synchronized: True if laserhead is synchronized by photodiode ''' if data is None: command = [COMMANDS.READ] + WORD_BYTES*[0] data = (yield from self.send_command(command)) dct = {} # 9 bytes are returned # the state is decoded from byte 7 and 8, i.e. -2 and -1 bits = "{:08b}".format(data[-1]) dct['parsing'] = int(bits[STATE.PARSING]) dct['error'] = int(bits[STATE.ERROR]) dct['mem_full'] = int(bits[STATE.FULL]) bits = "{:08b}".format(data[-2]) mapping = list(self.platform.stepspermm.keys()) for i in range(self.platform.motors): dct[mapping[i]] = int(bits[i]) dct['photodiode_trigger'] = int(bits[self.platform.motors]) dct['synchronized'] = int(bits[self.platform.motors+1]) return dct @property def position(self): '''retrieves position from FPGA and updates internal position position is stored on the FPGA in steps position is stored on object in mm return positions as np.array in mm order is [x, y, z] ''' command = [COMMANDS.POSITION] + WORD_BYTES*[0] for i in range(self.platform.motors): read_data = (yield from self.send_command(command)) self._position[i] = unpack('!q', read_data[1:])[0] # step --> mm self._position = (self._position / np.array(list(self.platform.stepspermm.values()))) return self._position @property def enable_steppers(self): '''returns 1 if steppers are enabled, 0 otherwise The enable pin for the stepper drivers is not routed via FPGA. The enable pin is low if enabled. Enabled stepper motor do not move if the FPGA is not parsing instructions from FIFO. ''' return not self.enable.value @enable_steppers.setter def enable_steppers(self, val): '''set enable pin stepper motor drivers and parsing FIFO buffer by FPGA val -- boolean, True enables steppers ''' assert type(val) == bool if val: self.enable.off() self.spi_exchange_data([COMMANDS.START]+WORD_BYTES*[0]) else: self.enable.on() self.spi_exchange_data([COMMANDS.STOP]+WORD_BYTES*[0]) @property def laser_current(self): '''return laser current per channel as integer both channels have the same current integer ranges from 0 to 255 where 0 no current and 255 full driver current ''' if upython: data = bytearray(1) self.bus.recv(data) else: data = self.bus.read_byte_data(self.platform.ic_address, 0) return data @laser_current.setter def laser_current(self, val): '''sets maximum laser current of laser driver per channel This does not turn on or off the laser. Laser is set to this current if pulsed. Laser current is set by enabling one or two channels. Second by setting a value between 0-255 at the laser driver chip for the laser current. The laser needs a minimum current. ''' if val < 0 or val > 150: # 255 kills laser at single channel raise Exception('Invalid or too high laser current') if upython: self.bus.mem_write(val, self.platform.ic_address) else: self.bus.write_byte_data(self.platform.ic_address, 0, val) def set_parsing(self, value): '''enables or disables parsing of FIFO by FPGA val -- True FPGA parses FIFO False FPGA does not parse FIFO ''' assert type(value) == bool if value: command = [COMMANDS.START] else: command = [COMMANDS.STOP] command += WORD_BYTES*[0] return (yield from self.send_command(command)) def home_axes(self, axes, speed=None, displacement=-200): '''home given axes, i.e. [1,0,1] homes x, z and not y axes -- list with axes to home speed -- speed in mm/s used to home displacement -- displacement used to touch home switch ''' assert len(axes) == self.platform.motors dist = np.array(axes)*np.array([displacement]*self.platform.motors) yield from self.gotopoint(position=dist.tolist(), speed=speed, absolute=False) # TODO: this is strange, should it be here # on the board steps and count is stored # you could move this to spline_coefficients # the flow over method of a certain bit comes from beagleg def steps_to_count(self, steps): '''compute count for a given number of steps steps -- motor moves in small steps Shift is needed as two ticks per step are required You need to count slightly over the threshold. That is why +1 is added. ''' bitshift = bit_shift(self.platform) count = (steps << (1+bitshift))+(1 << (bitshift-1)) return count def gotopoint(self, position, speed=None, absolute=True): '''move machine to position or with displacement at constant speed Axes are moved independently to simplify the calculation. The move is carried out as a first order spline, i.e. only velocity. position -- list with position or displacement in mm for each motor speed -- list with speed in mm/s, if None default speeds used absolute -- True if position, False if displacement ''' assert len(position) == self.platform.motors if speed is not None: assert len(speed) == self.platform.motors else: speed = [10]*self.platform.motors # conversions to steps / count give rounding errors # minimized by setting speed to integer speed = np.absolute(np.array(speed)) displacement = np.array(position) if absolute: # TODO: position machine should be in line with self._position # which to pick? displacement -= self._position homeswitches_hit = [0]*len(position) for idx, disp in enumerate(displacement): if disp == 0: # no displacement, go to next axis continue # Time needed for move # unit oscillator ticks (times motor position is updated) time = abs(disp/speed[idx]) ticks_total = (time*MOTORFREQ).round().astype(int) # mm -> steps steps_per_mm = list(self.platform.stepspermm.values())[idx] speed_steps = int(round(speed[idx] * steps_per_mm*np.sign(disp))) speed_cnts = self.steps_to_count(speed_steps)/MOTORFREQ velocity = np.zeros_like(speed).astype('int64') velocity[idx] = speed_cnts if self.test: (yield from self.set_parsing(True)) else: self.set_parsing(True) while ticks_total > 0: ticks_move = \ MOVE_TICKS if ticks_total >= MOVE_TICKS else ticks_total # execute move and retrieve if switch is hit switches_hit = (yield from self.spline_move(int(ticks_move), velocity.tolist())) ticks_total -= ticks_move # move is aborted if home switch is hit and # velocity is negative cond = (switches_hit[idx] == 1) & (np.sign(disp) < 0) if cond: break # update internally stored position self._position += displacement # set position to zero if home switch hit self._position[homeswitches_hit == 1] = 0 def send_command(self, command, blocking=False): '''writes command to spi port blocking -- try again if memory is full returns bytearray with length equal to data sent ''' def send_command(command): assert len(command) == WORD_BYTES+COMMAND_BYTES if self.test: data = (yield from self.spi_exchange_data(command)) else: data = (self.spi_exchange_data(command)) return data if blocking: trials = 0 while True: trials += 1 data = (yield from send_command(command)) state = (yield from self.get_state(data)) if state['error']: raise Exception("Error detected on FPGA") if not state['mem_full']: break if trials > self.maxtrials: raise Memfull(f"Too many trials {trials} needed") else: data = (yield from send_command(command)) return data def enable_comp(self, laser0=False, laser1=False, polygon=False, synchronize=False): '''enable components FPGA does need to be parsing FIFO These instructions are executed directly. laser0 -- True enables laser channel 0 laser1 -- True enables laser channel 1 polygon -- False enables polygon motor ''' laser0, laser1, polygon = (int(bool(laser0)), int(bool(laser1)), int(bool(polygon))) synchronize = int(bool(synchronize)) data = ([COMMANDS.WRITE] + [0]*(WORD_BYTES-2) + [int(f'{synchronize}{polygon}{laser1}{laser0}', 2)] + [INSTRUCTIONS.WRITEPIN]) yield from self.send_command(data, blocking=True) def spline_move(self, ticks, coefficients): '''write spline move instruction with ticks and coefficients to FIFO If you have 2 motors and execute a second order spline You send 4 coefficients. If the controller supports a third order spline, remaining coefficients are padded as zero. User needs to submit all coefficients up to highest order used. ticks -- number of ticks in move, integer coefficients -- coefficients for spline move per axis, list returns array with zero if home switch is hit ''' platform = self.platform # maximum allowable ticks is move ticks, # otherwise counters overflow in FPGA assert ticks <= MOVE_TICKS assert len(coefficients) % platform.motors == 0 write_byte = COMMANDS.WRITE.to_bytes(1, 'big') move_byte = INSTRUCTIONS.MOVE.to_bytes(1, 'big') commands = [write_byte + ticks.to_bytes(7, 'big') + move_byte] # check max order given by caller of function max_coeff_order = (len(coefficients)//platform.motors) # prepare commands for motor in range(platform.motors): for degree in range(platform.poldegree): # set to zero if coeff not provided by caller if degree > max_coeff_order-1: coeff = 0 else: idx = degree+motor*max_coeff_order coeff = coefficients[idx] data = coeff.to_bytes(8, 'big', signed=True) commands += [write_byte + data] # send commands to FPGA for command in commands: data_out = (yield from self.send_command(command, blocking=True)) state = (yield from self.get_state(data_out)) axes_names = list(platform.stepspermm.keys()) return np.array([state[key] for key in axes_names]) def spi_exchange_data(self, data): '''writes data to peripheral data -- command followed with word list of multiple bytes returns bytearray with length equal to data sent ''' assert len(data) == (COMMAND_BYTES + WORD_BYTES) self.chip_select.off() # spidev changes values passed to it if not upython: from copy import deepcopy datachanged = deepcopy(data) response = bytearray(self.spi.xfer2(datachanged)) else: response = bytearray(data) self.spi.write_readinto(data, response) self.chip_select.on() return response def writeline(self, bitlst, stepsperline=1, direction=0): '''write bits to FIFO bit list bits which are written to substrate at the moment laser can only be on of off if bitlst is empty stop command is sent stepsperline stepsperline, should be greater than 0 if you don't want to move simply disable motor direction motor direction of scanning axis ''' bytelst = self.bittobytelist(bitlst, stepsperline, direction) write_byte = COMMANDS.WRITE.to_bytes(1, 'big') for i in range(0, len(bytelst), 8): lst = bytelst[i:i+8] lst.reverse() data = write_byte + bytes(lst) (yield from self.send_command(data, blocking=True)) def bittobytelist(self, bitlst, stepsperline=1, direction=0, bitorder='little'): '''converts bitlst to bytelst bit list bits which are written to substrate at the moment laser can only be on of off if bitlst is empty stop command is sent stepsperline stepsperline, should be greater than 0 if you don't want to move simply disable motor direction motor direction of scanning axis ''' # the halfperiod is sent over # this is the amount of ticks in half a cycle of # the motor # watch out for python "banker's rounding" # sometimes target might not be equal to steps bits = self.laser_params['BITSINSCANLINE'] halfperiod = int((bits-1) // (stepsperline*2)) if (halfperiod < 1): raise Exception("Steps per line cannot be achieved") # TODO: is this still an issue? # you could check as follows # steps = self.laser_params['TICKSINFACET']/(halfperiod*2) # print(f"{steps} is actual steps per line") direction = [int(bool(direction))] def remainder(bytelst): rem = (len(bytelst) % WORD_BYTES) if rem > 0: res = WORD_BYTES - rem else: res = 0 return res if len(bitlst) == 0: bytelst = [INSTRUCTIONS.LASTSCANLINE] bytelst += remainder(bytelst)*[0] else: assert len(bitlst) == self.laser_params['BITSINSCANLINE'] assert max(bitlst) <= 1 assert min(bitlst) >= 0 bytelst = [INSTRUCTIONS.SCANLINE] halfperiodbits = [int(i) for i in bin(halfperiod)[2:]] halfperiodbits.reverse() assert len(halfperiodbits) < 56 bytelst += np.packbits(direction+halfperiodbits, bitorder=bitorder).tolist() bytelst += remainder(bytelst)*[0] bytelst += np.packbits(bitlst, bitorder=bitorder).tolist() bytelst += remainder(bytelst)*[0] return bytelst