diff --git a/quadtank/avr/Makefile b/quadtank/avr/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..44e1f07816e54ba4befef1609a8e95a3b5edff05 --- /dev/null +++ b/quadtank/avr/Makefile @@ -0,0 +1,21 @@ +TARGETS=quadtank quadtank_32 + +quadtank.ARCH=avr +quadtank.CHIP=atmega16 +# 14.7456 MHz crystal, brown out +quadtank.FUSE=--wr_fuse_l=0x1f --wr_fuse_h=0xd9 --wr_fuse_e=0xff +quadtank.FUSE_L=0x1f +quadtank.FUSE_H=0xd9 +quadtank.C=quadtank +quadtank.H=../../lib/avr/serialio + +quadtank_32.ARCH=avr +quadtank_32.CHIP=atmega32 +# 14.7456 MHz crystal, brown out +quadtank_32.FUSE=--wr_fuse_l=0x1f --wr_fuse_h=0xd9 --wr_fuse_e=0xff +quadtank_32.FUSE_L=0x1f +quadtank_32.FUSE_H=0xd9 +quadtank_32.C=quadtank +quadtank_32.H=../../lib/avr/serialio + +include ../../lib/avr/Makefile.common diff --git a/quadtank/avr/calibrator b/quadtank/avr/calibrator new file mode 100755 index 0000000000000000000000000000000000000000..bd76419e3bbf2caa35c24c6306fd6285795b3222 --- /dev/null +++ b/quadtank/avr/calibrator @@ -0,0 +1,466 @@ +#!/usr/bin/python3 + +import serial +import sys +import time +import numpy +import traceback +import threading +import os + +class Opcom: + """ Layout: + Scroll line 1 + ... + Scroll line N-2 + Pressures + Action Status + """ + def __init__(self, f=sys.stdout): + self.f = os.fdopen(f.fileno(), 'bw') + self.f.write(b'\n') + self._pressures = b'' + self._action = b'' + self._status = b'' + + def up_left(self, n): + self.f.write(b'\033[%dA\r' % n) + + def down_left(self, n): + self.f.write(b'\033[%dB\r' % n) + + def progress(self, msg): + # Row 0..N-2 + self.up_left(1) + self.f.write(b' ' * len(self._pressures) + b'\r') + self.f.write(msg.encode('ascii') + b'\n') + self.f.write(b' ' * len(self._action) + b'\r') + self.f.write(self._pressures + b'\n') + self.f.write(self._action + b' ' * (40 - len(self._action))) + self.f.write(self._status + b'\r') + self.f.flush() + pass + + def pressures(self, msg): + # Row N-1 + self.up_left(1) + self.f.write(msg.encode('ascii') + + b' ' * (len(self._pressures) - len(msg))) + self.down_left(1) + self._pressures = msg.encode('ascii') + self.f.flush() + pass + + def action(self, msg): + # Row N, 0..39 + self.f.write(b'\r') + self.f.write(msg.encode('ascii') + + b' ' * (len(self._action) - len(msg)) + b'\r') + self._action = msg.encode('ascii') + self.f.flush() + pass + + def status(self, msg): + # Row N, 40.. + self.f.write(b'\r') + self.f.write(b' ' * (40 + len(self._status)) + b'\r') + self.f.write(self._action + b' ' * (40 - len(self._action))) + self.f.write(msg.encode('ascii') + b'\r') + self._status = msg.encode('ascii') + self.f.flush() + pass + +KIND = { + 0: None, + 1: "DigitalIn", + 2: "DigitalOut", + 3: "AnalogIn", + 4: "AnalogOut", + 5: "Counter" + } +CMD = { + 0: "Bits", + 1: "Min", + 2: "Max", + } + +class SerialIO: + def __init__(self, port): + self.config = None + self.channel = {} + self.tty = serial.Serial(port, 115200) + self.cond = threading.Condition() + t = threading.Thread(target=self.read) + t.setDaemon(True) + t.start() + self.pollchannel(31) + self.cond.acquire() + self.cond.wait(2) + self.cond.release() + + def pollchannel(self, index): + self.cond.acquire() + self.channel[index] = None + self.tty.write(bytes([0x60 | index])) + self.cond.release() + + def getchannel(self, index): + self.cond.acquire() + while self.channel[index] == None: + self.cond.wait() + result = self.channel[index] + self.cond.release() + return result + + def readchannel(self, index): + self.pollchannel(index) + return self.getchannel(index) + + def setchannel(self, index, value, bound=0x3ff): + value = int(max(0, min(value, bound))) + self.cond.acquire() + if value >= (1<<30): + self.tty.write(bytes([0x80 | ((value >> 30) & 0x03)])) + if value >= (1<<23): + self.tty.write(bytes([0x80 | ((value >> 23) & 0x7f)])) + if value >= (1<<16): + self.tty.write(bytes([0x80 | ((value >> 16) & 0x7f)])) + if value >= (1<<9): + self.tty.write(bytes([0x80 | ((value >> 9) & 0x7f)])) + self.tty.write(bytes([0x80 | ((value >> 2) & 0x7f)])) + self.tty.write(bytes([((value << 5) & 0x60) | (index & 0x1f)])) + self.cond.release() + + def read(self): + value = 0 + n = 0 + config = {} + while True: + ch = self.tty.read(1) + + value = value << 7 | ord(ch) & 0x7f + n += 1 + if ord(ch) & 0x80 == 0: + # Last byte, so lets handle it + if n == 1: + # Digital I/O or poll + pass + else: + channel = value & 0x1f + value = value >> 5 + if channel != 31: + self.cond.acquire() + self.channel[channel] = value + self.cond.notifyAll() + self.cond.release() + else: + if self.handleconfig(value, config): + self.cond.acquire() + self.config = config + self.cond.notifyAll() + self.cond.release() + value = 0 + n = 0 + + def handleconfig(self, value, config): + channel = value & 0x1f + kind = (value >> 5) & 0x07 + cmd = (value >> 8) & 0x03 + value = value >> 10 + if KIND[kind] == None: + return True + else: + try: + config[KIND[kind]] + except: + config[KIND[kind]] = {} + try: + config[KIND[kind]][channel] + except: + config[KIND[kind]][channel] = {} + + config[KIND[kind]][channel][CMD[cmd]] = value + +class QuadTank: + NOISE = 10 + A_PUMP = 0 + B_PUMP = 1 + A1_LEVEL = 0 + A2_LEVEL = 1 + B1_LEVEL = 2 + B2_LEVEL = 3 + A_FLOW = 4 + B_FLOW = 5 + P_LABEL = [ "A1", "A2", "B1", "B2", "AFlow", "BFlow" ] + + def __init__(self, port, opcom): + self.io = SerialIO(port) + self.opcom = opcom + self.get_pressures() + + def set_pump_voltage(self, voltage_A=0, voltage_B=0): + self.voltage_A = voltage_A + self.voltage_B = voltage_B + self.io.setchannel(self.A_PUMP, voltage_A) + self.io.setchannel(self.B_PUMP, voltage_B) + + def get_pressures(self): + p = numpy.array([self.io.readchannel(self.A1_LEVEL), + self.io.readchannel(self.A2_LEVEL), + self.io.readchannel(self.B1_LEVEL), + self.io.readchannel(self.B2_LEVEL), + self.io.readchannel(self.A_FLOW), + self.io.readchannel(self.B_FLOW)]) + msg = '' + for i in range(len(p)): + msg += 'P%d/%s=%4d ' % (i, self.P_LABEL[i], p[i]) + self.opcom.pressures(msg) + return p + + def attention(self, msg="", voltage_A=1023, voltage_B=1023): + self.opcom.action(msg) + self.set_pump_voltage(voltage_A, voltage_B) + time.sleep(0.3) + self.set_pump_voltage(0, 0) + time.sleep(2) + + def drain(self): + # Drained when all deltas are below noise for 2 seconds + self.set_pump_voltage(0, 0) + old = self.get_pressures() + steady = 0 + while steady < 4: + time.sleep(0.5) + new = self.get_pressures() + + diff = numpy.abs(new - old) + old = new + self.opcom.status("Draining %d/4 (diff=%d)" % (steady, diff.max())) + if diff.max() < self.NOISE and new[0:4].max() < 200: + steady = steady + 1 + self.opcom.action("") + else: + steady = 0 + msg = "" + name = [ "AV3", "AV4", "BV3", "BV4" ] + for i in range(0,4): + if diff[i] < self.NOISE and new[i] > 200: + msg += name[i] + "/" + if len(msg): + self.attention("Open %s" % msg[0:-1]) + else: + self.opcom.action("") + self.opcom.action("") + self.opcom.status("") + + def check_valves(self): + self.opcom.progress("Checking valves") + # Fill tanks with some water + self.set_pump_voltage(1023, 1023) + time.sleep(3) + restart = True + while restart: + restart = False + self.drain() + targets = [ type("", (), + dict(voltage_A = 1023, + voltage_B = 0, + top = self.A1_LEVEL, + bottom = self.A2_LEVEL, + decoupled = self.B2_LEVEL, + msg_open_upper = "Open AV3", + msg_open_lower = "Open AV4", + msg_close = "Close V5/BV1")), + type("", (), + dict(voltage_A = 0, + voltage_B = 1023, + top = self.B1_LEVEL, + bottom = self.B2_LEVEL, + decoupled = self.A2_LEVEL, + msg_open_upper = "Open BV3", + msg_open_lower = "Open BV4", + msg_close = "Close V5/AV1"))] + for t in targets: + self.opcom.action("") + if restart: + break + self.set_pump_voltage(t.voltage_A, t.voltage_B) + t0 = time.time() + p0 = self.get_pressures() + drain_decoupled = False + drain_bottom = False + while True: + time.sleep(0.2) + p = self.get_pressures() + diff = p - p0 + if diff[t.decoupled] > self.NOISE: + self.opcom.action(t.msg_close) + drain_decoupled = True + elif drain_decoupled: + if diff[t.decoupled] < self.NOISE: + restart = True + break + elif diff[t.top] > 400 and diff[t.bottom] < self.NOISE: + self.opcom.action(t.msg_open_upper) + elif not drain_bottom and diff[t.bottom] > 200: + drain_bottom = True + self.set_pump_voltage() + self.opcom.action(t.msg_open_lower) + elif drain_bottom: + if diff[t.bottom] < self.NOISE: + break + + def calibrated_flow(self): + self.opcom.progress("Calibrating flow") + voltage_A = 100 + voltage_B = 100 + done = False + while not done: + done = True + self.set_pump_voltage(voltage_A, voltage_B) + time.sleep(1) + p = self.get_pressures() + if p[self.A_FLOW] < 180: + voltage_A = voltage_A + 20 + done = False + if p[self.B_FLOW] < 180: + voltage_B = voltage_B + 20 + done = False + old = self.get_pressures() + steady = 0 + sum = numpy.array([0, 0]) + n = 0 + while steady < 4: + new = self.get_pressures() + sum = sum + new[4:6] + n += 1 + if n % 20 == 0: + diff = numpy.abs(new - old)[0:4].max() + old = new + self.opcom.status("Calibrating flow %d/4 (diff=%d)" % + (steady, diff)) + if diff < self.NOISE: + steady = steady + 1 + else: + steady = 0 + time.sleep(0.1) + self.opcom.status("") + return sum / n + + def max_levels(self): + self.opcom.progress("Getting max levels") + old = self.get_pressures()[0:4] + voltage_A = self.voltage_A + voltage_B = self.voltage_B + low = True + while True: + time.sleep(0.5) + low = not low + if low: + self.set_pump_voltage(voltage_A - 40, voltage_B - 40) + else: + self.set_pump_voltage(voltage_A, voltage_B) + new = self.get_pressures()[0:4] + diff = (new - old) + if diff[self.A2_LEVEL] < 100 and diff[self.B2_LEVEL] < 100: + self.opcom.action("Close AV4/BV4") + elif diff[self.A2_LEVEL] < 100: + self.opcom.action("Close AV4") + elif diff[self.B2_LEVEL] < 100: + self.opcom.action("Close BV4") + else: + break + self.opcom.action("") + self.set_pump_voltage(voltage_A=1023, voltage_B=1023) + old = self.get_pressures()[0:4] + steady = 0 + while steady < 4: + new = self.get_pressures()[0:4] + diff = numpy.abs(new - old).max() + old = new + self.opcom.status("Calibrating top %d/4 (diff=%d)" % (steady, diff)) + if diff < self.NOISE: + steady = steady + 1 + else: + steady = 0 + time.sleep(0.5) + + def calibrate(self): + # Calibration steps: + # 1. Check valves + # 2. Drain + # 3. Save min levels + # 4. Calibrate flows + # 5. Save mid levels + # 6. Find max levels + # 7. Save max levels + # 8. Save configuration + + self.check_valves() + self.drain() + # All valves except AV2/BV2 are in known positions, tanks are empty + min_pressure = self.get_pressures() + calibrated_flow = self.calibrated_flow() + mid_pressure = self.get_pressures() + self.max_levels() + max_pressure = self.get_pressures() + self.opcom.progress("All measurements done") + self.opcom.status("") + self.opcom.action("") + mid_level = [ 0, 0, 0, 0 ] + calib = [] + for i in range(0,4): + # 0V at 0 mm, 10V at 200 mm + min = min_pressure[i] + mid = mid_pressure[i] + max = max_pressure[i] + n = max - min + step = 197.0 / n + low = -min * step + high = 197.0 + (1023 - max) * step + mid_level[i] = low + mid * step + calib.append([low * 10 / 200, high * 10 / 200]) + for i in range(0,2): + # Flow sensors follows the same sqrt law as level + # calibration values are such that 5V corresponds to the + # flow that gives 200 mm level at stationarity + min = min_pressure[i + 4] + max = calibrated_flow[i] + n = max - min + step = mid_level[i * 2] / n + low = -min * step + high = mid_level[i * 2] + (1023 - max) * step + calib.append([low * 5 / 200, high * 5 / 200]) + for i in range(len(calib)): + self.opcom.progress('%d %s' % (i, calib[i])) + self.opcom.progress("Min=%s" % min_pressure) + self.opcom.progress("Mid=%s" % mid_pressure) + self.opcom.progress("Max=%s" % max_pressure) + self.opcom.progress("imbalance A %f" % + (mid_pressure[0]/mid_pressure[1])) + self.opcom.progress("imbalance B %f" % + (mid_pressure[2]/mid_pressure[3])) + self.drain() + for i in range(len(calib)): + self.write_calibration(i * 2, calib[i][0]) + self.write_calibration(i * 2 + 1, calib[i][1]) + self.write_calibration(12, 0) + + def write_calibration(self, index, value): + if value < 0: + tmp = (int(-value * 1000) << 8) | 0x80 | index + else: + tmp = (int(value * 1000) << 8) | index + self.opcom.progress("%d %8x" % (index, tmp)) + self.io.setchannel(31, tmp, 0xffffffff) + +if __name__ == "__main__": + opcom = Opcom(sys.stdout) + tank = QuadTank(sys.argv[1], opcom) + try: + tank.calibrate() + except: + traceback.print_exc() + + tank.io.setchannel(tank.A_PUMP, 0) + tank.io.setchannel(tank.B_PUMP, 0) diff --git a/quadtank/avr/quadtank.c b/quadtank/avr/quadtank.c new file mode 100644 index 0000000000000000000000000000000000000000..02961ab7ee39dd500636a5b0f2e8944545f58b1b --- /dev/null +++ b/quadtank/avr/quadtank.c @@ -0,0 +1,269 @@ +#include <avr/eeprom.h> +#include <avr/io.h> +#include <avr/interrupt.h> +//#include <avr/signal.h> +#include <inttypes.h> +#include "serialio.h" + +volatile unsigned char error; + + +volatile unsigned char serial_readbits; +volatile unsigned char serial_readchannels; +volatile unsigned char serial_readconfig; +struct values { + int A1_level; + int A2_level; + int flow_A; + int B1_level; + int B2_level; + int flow_B; + uint8_t timer; +}; +struct bounds { + unsigned char not_valid; + struct { + int min; + int max; + } A1, A2, flow_A, B1, B2, flow_B; +}; +volatile struct values global; +volatile struct bounds bounds; + +SIGNAL(ADC_vect) +{ + unsigned char channel = ADMUX & 0x0f; + unsigned int value = ADCW; + + if (global.timer) { + global.timer--; + PORTD &= ~0x40; + } else { + PORTD |= 0x40; + } + switch (channel) { + case 0: { + channel = 1; + global.flow_B = value; + } break; + case 1: { + channel = 2; + global.B1_level = value; + } break; + case 2: { + channel = 3; + global.B2_level = value; + } break; + case 3: { + channel = 4; + global.A1_level = value; + } break; + case 4: { + channel = 5; + global.A2_level = value; + } break; + case 5: { + channel = 0; + global.flow_A = value; + } break; + default: { + channel = 0; + } break; + } + ADMUX = 0x40 | channel; // Vref = Vcc, right adjust + ADCSRA = 0xcf; // Enable ADC interrupts, Clock/128 +} + +typedef enum { cmd_clear_bit, cmd_set_bit, + cmd_read_bit, cmd_read_chan } command; + +SIGNAL(USART_RXC_vect) +{ + char ch = UDR; + + global.timer = 0xff; + switch (serialio_RXC(ch)) { + case serialio_clearbit: { + switch (serialio_channel) { + } + } break; + case serialio_setbit: { + switch (serialio_channel) { + } + } break; + case serialio_pollbit: { + } break; + case serialio_pollchannel: { + if (serialio_channel < 6) { + serial_readchannels |= (1<<serialio_channel); + } else if (serialio_channel == 31) { + serial_readconfig = 1; + } + } break; + case serialio_setchannel: { + switch (serialio_channel) { + case 0: { + OCR1B = (0x3ff - serialio_value) & 0x3ff; + } break; + case 1: { + OCR1A = serialio_value & 0x3ff; + } break; + case 31: { + unsigned char pos = serialio_value & 0x7f; + unsigned char sign = serialio_value & 0x80; + int value = serialio_value >> 8; + if (sign) { + value = -value; + } + switch (pos) { + case 0: bounds.A1.min = value; break; + case 1: bounds.A1.max = value; break; + case 2: bounds.A2.min = value; break; + case 3: bounds.A2.max = value; break; + case 4: bounds.B1.min = value; break; + case 5: bounds.B1.max = value; break; + case 6: bounds.B2.min = value; break; + case 7: bounds.B2.max = value; break; + case 8: bounds.flow_A.min = value; break; + case 9: bounds.flow_A.max = value; break; + case 10: bounds.flow_B.min = value; break; + case 11: bounds.flow_B.max = value; break; + case 12: eeprom_write_block((void*)&bounds, 0, sizeof(bounds)); + } + } break; + } break; + } break; + case serialio_error: { + } break; + case serialio_more: { + } break; + } +} + +static void conf_min(int channel, int value); +static void conf_max(int channel, int value); + +/* + * PA0 Flow B + * PA1 Level B1 + * PA2 Level B2 + * PA3 Level A1 + * PA4 Level A2 + * PA5 Flow A + * + * PD4 Pump A + * PD5 Pump B + * PD6 LED 1 + * PD7 LED 2 + * + */ +int main() +{ + serialio_init(); + serial_readbits = 0; + serial_readchannels = 0; + serial_readconfig = 0; + PORTA = 0x00; // PortA, pull-ups + DDRA = 0x00; // PortA, all inputs + PORTD = 0x40; // PortD, pull-ups / initial values + DDRD = 0xf0; // PortD, 4-7 outputs + TCCR0 = 0x05; // Timer0, Clock / 1024 + TCCR1A = 0xb3; // OC1A & OC1B 10 bit PWM (PC), clear A on match + // set B on match + TCCR1B = 0x01; // Clock / 1 + UCSRA = 0x00; // USART: + UCSRB = 0x98; // USART: RxIntEnable|RxEnable|TxEnable + UCSRC = 0x86; // USART: 8bit, no parity + UBRRH = 0; // USART: 115200 @ 14.7456MHz + UBRRL = 7; // USART: 115200 @ 14.7456MHz + ADMUX = 0x40; // Vref = Vcc, right adjust + ADMUX = 0x4e; // Vref = Vcc, right adjust, read 1.22V (Vbg) + ADCSRA = 0xcf; // Enable ADC interrupts, Clock/128 + OCR1A = 0 & 0x3ff; + OCR1B = (0x3ff - 0) & 0x3ff; + + eeprom_read_block((void*)&bounds, 0, sizeof(bounds)); + if (bounds.not_valid) { + bounds.A1.min = 0; + bounds.A1.max = 10000; + bounds.A2.min = 0; + bounds.A2.max = 10000; + bounds.flow_A.min = 0; + bounds.flow_A.max = 10000; + bounds.B1.min = 0; + bounds.B1.max = 10000; + bounds.B2.min = 0; + bounds.B2.max = 10000; + bounds.flow_B.min = 0; + bounds.flow_B.max = 10000; + bounds.not_valid = 0; + } + + SREG = 0x80; // Global interrupt enable + while (1) { + unsigned char channels, config; + struct values local; + + SREG = 0x00; // Global interrupt disable + serial_readbits = 0; + channels = serial_readchannels; + serial_readchannels = 0; + config = serial_readconfig; + serial_readconfig = 0; + local = global; + SREG = 0x80; // Global interrupt enable + if (channels & 0x01) { serialio_putchannel(0, local.A1_level); } + if (channels & 0x02) { serialio_putchannel(1, local.A2_level); } + if (channels & 0x04) { serialio_putchannel(2, local.B1_level); } + if (channels & 0x08) { serialio_putchannel(3, local.B2_level); } + if (channels & 0x10) { serialio_putchannel(4, local.flow_A); } + if (channels & 0x20) { serialio_putchannel(5, local.flow_B); } + if (config) { + CONF_ANALOG_IN(0, CONF_RESOLUTION(10)); // Level A1 + conf_min(0, bounds.A1.min); + conf_max(0, bounds.A1.max); + CONF_ANALOG_IN(1, CONF_RESOLUTION(10)); // Level A2 + conf_min(1, bounds.A2.min); + conf_max(1, bounds.A2.max); + CONF_ANALOG_IN(2, CONF_RESOLUTION(10)); // Level B1 + conf_min(2, bounds.B1.min); + conf_max(2, bounds.B1.max); + CONF_ANALOG_IN(3, CONF_RESOLUTION(10)); // Level B2 + conf_min(3, bounds.B2.min); + conf_max(3, bounds.B2.max); + CONF_ANALOG_IN(4, CONF_RESOLUTION(10)); // Flow left + conf_min(4, bounds.flow_A.min); + conf_max(4, bounds.flow_A.max); + CONF_ANALOG_IN(5, CONF_RESOLUTION(10)); // Flow right + conf_min(5, bounds.flow_B.min); + conf_max(5, bounds.flow_B.max); + CONF_ANALOG_OUT(0, CONF_RESOLUTION(10)); // Pump A + CONF_ANALOG_OUT(0, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(0))); + CONF_ANALOG_OUT(0, CONF_MAX(CONF_POSITIVE_MILLIVOLT(10000))); + CONF_ANALOG_OUT(1, CONF_RESOLUTION(10)); // Pump B + CONF_ANALOG_OUT(1, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(0))); + CONF_ANALOG_OUT(1, CONF_MAX(CONF_POSITIVE_MILLIVOLT(10000))); + + CONF_END(); + } + } +} + +static void conf_min(int channel, int value) +{ + if (value < 0) { + CONF_ANALOG_IN(channel, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(-value))); + } else { + CONF_ANALOG_IN(channel, CONF_MIN(CONF_POSITIVE_MILLIVOLT(value))); + } +} + +static void conf_max(int channel, int value) +{ + if (value < 0) { + CONF_ANALOG_IN(channel, CONF_MAX(CONF_NEGATIVE_MILLIVOLT(-value))); + } else { + CONF_ANALOG_IN(channel, CONF_MAX(CONF_POSITIVE_MILLIVOLT(value))); + } +} +