├── README.md ├── emulate.py ├── fsm.py ├── logic.py └── wc1.py /README.md: -------------------------------------------------------------------------------- 1 | # py_plc 2 | 3 | Python code to help with PLC ladder logic programming. 4 | 5 | ## Motivation 6 | 7 | PLC programming is typically specified with "ladder logic"- a diagramatic form for boolean sum of products (DNF) equations. Specifying these directly is tedious and error prone. 8 | 9 | The PLC industry has not really caught up with the idea of programming in high level languages. Industrial electricians understand wires and relays but not programming languages. The host based tools can emulate the PLCs offline but still don't provide a real programming language for 10 | logic specification. 11 | 12 | This is code to bridge the gap between a high level, name based specification of the FSM and a low level bit oriented specification in the PLC. 13 | 14 | ## How to use 15 | 16 | Write an FSM in high level python (E.g. wc1.py). 17 | 18 | emulate.py: 19 | 20 | 1. run the state machine using the keyboard for real time input 21 | 2. validate correct operation 22 | 23 | logic.py: 24 | 25 | 1. apply the complete set of state and input bit vectors to the FSM 26 | 2. record the next state and output values 27 | 3. work out the minimised logic functions 28 | 4. display the result 29 | 30 | -------------------------------------------------------------------------------- /emulate.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | #------------------------------------------------------------------------------ 3 | 4 | 5 | #------------------------------------------------------------------------------ 6 | 7 | import pygame 8 | from pygame.locals import * 9 | 10 | import wc1 11 | 12 | #------------------------------------------------------------------------------ 13 | 14 | _screen_x = 300 15 | _screen_y = 300 16 | 17 | _pushbutton = 0 18 | _toggle = 1 19 | 20 | #------------------------------------------------------------------------------ 21 | 22 | def inv(x): 23 | return ~x & 1 24 | 25 | class plc: 26 | 27 | def __init__(self, fsm, key_map): 28 | pygame.init() 29 | self.screen = pygame.display.set_mode((_screen_x, _screen_y)) 30 | pygame.display.set_caption('PLC') 31 | self.fsm = fsm 32 | self.key_map = key_map 33 | self.sv = fsm.s.isv 34 | self.iv = fsm.i.iiv 35 | self.ov = fsm.o.ov 36 | 37 | def __str__(self): 38 | return str(self.fsm) 39 | 40 | def get_inputs(self): 41 | """work out the current input vector""" 42 | for event in pygame.event.get(): 43 | if event.type in (KEYDOWN, KEYUP): 44 | if self.key_map[event.key]: 45 | (bit, switch) = self.key_map[event.key] 46 | if event.type == KEYDOWN: 47 | self.iv[bit] = inv(self.iv[bit]) 48 | elif event.type == KEYUP: 49 | if switch == _pushbutton: 50 | self.iv[bit] = inv(self.iv[bit]) 51 | 52 | def run(self): 53 | self.get_inputs() 54 | # run the state machine 55 | self.sv, self.ov = self.fsm.fsm(self.sv, self.iv) 56 | 57 | #------------------------------------------------------------------------------ 58 | 59 | def main(): 60 | 61 | key_map = { 62 | K_f: (0, _pushbutton), 63 | K_w: (1, _pushbutton), 64 | K_s: (2, _pushbutton), 65 | K_x: (3, _toggle), 66 | } 67 | 68 | x = plc(wc1.wc1(), key_map) 69 | prev_state = None 70 | state_count = 0 71 | 72 | while True: 73 | state = str(x) 74 | if state != prev_state: 75 | prev_state = state 76 | state_count += 1 77 | print '%d: %s' % (state_count, state) 78 | x.run() 79 | 80 | main() 81 | 82 | #------------------------------------------------------------------------------ 83 | 84 | -------------------------------------------------------------------------------- /fsm.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | """ 3 | 4 | general classes for finite state machines 5 | 6 | """ 7 | #------------------------------------------------------------------------------ 8 | 9 | def bin_tuple(x, n): 10 | l = [] 11 | for bit in range(n): 12 | l.append(x & 1) 13 | x >>= 1 14 | l.reverse() 15 | return tuple(l) 16 | 17 | #------------------------------------------------------------------------------ 18 | 19 | class inputs: 20 | """map input names to an input bit vector""" 21 | 22 | def __init__(self, iiv, names): 23 | assert (len(iiv) == len(names)), 'initial vector length != names length' 24 | self.iiv = iiv 25 | self.iv = iiv 26 | self.names = names 27 | self.n = len(iiv) 28 | self.name2bit = dict(zip(names, range(self.n))) 29 | 30 | def set_iv(self, iv): 31 | self.iv = iv 32 | 33 | def set(self, iv, name): 34 | self.iv[self.name2bit[name]] = 1 35 | 36 | def clr(self, name): 37 | self.iv[self.name2bit[name]] = 0 38 | 39 | def is_set(self, name): 40 | return self.iv[self.name2bit[name]] != 0 41 | 42 | def is_clr(self, name): 43 | return self.iv[self.name2bit[name]] == 0 44 | 45 | def __str__(self): 46 | s = ['%s:%d' % (n, (0,1)[self.is_set(n)]) for n in self.names] 47 | return ' '.join(s) 48 | 49 | #------------------------------------------------------------------------------ 50 | 51 | class outputs: 52 | """map output names to an output bit vector""" 53 | 54 | def __init__(self, names): 55 | self.names = names 56 | self.n = len(names) 57 | self.name2bit = dict(zip(names, range(self.n))) 58 | self.set_null() 59 | 60 | def set_null(self): 61 | self.ov = [0,] * self.n 62 | 63 | def set(self, name): 64 | self.ov[self.name2bit[name]] = 1 65 | 66 | def clr(self, name): 67 | self.ov[self.name2bit[name]] = 0 68 | 69 | def is_set(self, name): 70 | return self.ov[self.name2bit[name]] != 0 71 | 72 | def __str__(self): 73 | s = ['%s:%d' % (n, (0,1)[self.is_set(n)]) for n in self.names] 74 | return ' '.join(s) 75 | 76 | #------------------------------------------------------------------------------ 77 | 78 | class state: 79 | """map state names to the state vector""" 80 | 81 | def __init__(self, isv, names): 82 | assert (len(names) <= (1 << len(isv))), 'not enough bits in the state vector' 83 | self.isv = isv 84 | self.sv = self.isv 85 | self.names = names 86 | self.n = len(isv) 87 | vset = [bin_tuple(i, self.n) for i in range(len(names))] 88 | self.name2vector = dict(zip(names, vset)) 89 | self.vector2name = dict(zip(vset, names)) 90 | 91 | def set_sv(self, sv): 92 | self.sv = sv 93 | 94 | def in_state(self, name): 95 | """return True if we are in the named state""" 96 | return self.vector2name[self.sv] == name 97 | 98 | def state(self, name): 99 | """return the bit vector for this state""" 100 | return self.name2vector[name] 101 | 102 | def __str__(self): 103 | return self.vector2name[self.sv] 104 | 105 | #------------------------------------------------------------------------------ 106 | -------------------------------------------------------------------------------- /logic.py: -------------------------------------------------------------------------------- 1 | #! /opt/python3.4/bin/python3.4 2 | #------------------------------------------------------------------------------ 3 | 4 | from pyeda.inter import * 5 | 6 | import wc1 7 | 8 | #------------------------------------------------------------------------------ 9 | 10 | def bin_tuple(x, n): 11 | l = [] 12 | for bit in range(n): 13 | l.append(x & 1) 14 | x >>= 1 15 | l.reverse() 16 | return tuple(l) 17 | 18 | def main(): 19 | 20 | fsm = wc1.wc1() 21 | bits = fsm.s.n + fsm.i.n 22 | 23 | s_tt = [[] for i in range(fsm.s.n)] 24 | o_tt = [[] for i in range(fsm.o.n)] 25 | 26 | # run the state machine for all state and input combinations 27 | for i in range(1 << bits): 28 | v = bin_tuple(i, bits) 29 | # generate the state and input vector 30 | sv = v[0:fsm.s.n] 31 | iv = v[fsm.s.n:] 32 | # get the next state and output vector 33 | next_sv, ov = fsm.fsm(sv, iv) 34 | # store the next state 35 | for i in range(fsm.s.n): 36 | s_tt[i].append('%d' % next_sv[i]) 37 | # store the output 38 | for i in range(fsm.o.n): 39 | o_tt[i].append('%d' % ov[i]) 40 | #print('%s %s -> %s %s' % (sv, iv, next_sv, tuple(ov))) 41 | 42 | # setup the truth tables 43 | xvars = ttvars('x', bits) 44 | for i in range(fsm.s.n): 45 | s_tt[i] = truthtable(xvars, s_tt[i]) 46 | for i in range(fsm.o.n): 47 | o_tt[i] = truthtable(xvars, o_tt[i]) 48 | 49 | # minimise 50 | s_func = [espresso_tts(s_tt[i]) for i in range(fsm.s.n)] 51 | s_func = [x for (x,) in s_func] 52 | o_func = [espresso_tts(o_tt[i]) for i in range(fsm.o.n)] 53 | o_func = [x for (x,) in o_func] 54 | 55 | # stringify the functions 56 | s_func = [str(x) for x in s_func] 57 | o_func = [str(x) for x in o_func] 58 | 59 | # work out the x to plc name mapping 60 | x2plc = [] 61 | for i in range(fsm.s.n): 62 | x2plc.append('m%d' % (i + 1)) 63 | for i in range(fsm.i.n): 64 | x2plc.append('i%d' % (i + 1)) 65 | x2plc.reverse() 66 | 67 | # apply the mapping 68 | for i in range(len(x2plc)): 69 | s_func = [x.replace('x[%d]' % i, x2plc[i]) for x in s_func] 70 | o_func = [x.replace('x[%d]' % i, x2plc[i]) for x in o_func] 71 | 72 | # display the equations in plc form 73 | for i in range(fsm.s.n): 74 | print('m%d_next = %s' % (i + 1, s_func[i])) 75 | for i in range(fsm.o.n): 76 | print('q%d = %s' % (i + 1, o_func[i])) 77 | 78 | main() 79 | 80 | 81 | #------------------------------------------------------------------------------ 82 | -------------------------------------------------------------------------------- /wc1.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | """ 3 | 4 | well controller 1 5 | 6 | """ 7 | #------------------------------------------------------------------------------ 8 | 9 | import fsm 10 | 11 | #------------------------------------------------------------------------------ 12 | 13 | i_names = ('not_full', 'start_well', 'start_sj', 'stop') 14 | o_names = ('sj_pump', 'well_pump', 'chlorinator') 15 | s_names = ('full', 'fill_sj', 'fill_well', 'stopped') 16 | 17 | class wc1: 18 | 19 | def __init__(self): 20 | self.i = fsm.inputs([0,0,0,1], i_names) 21 | self.s = fsm.state((1,1), s_names) 22 | self.o = fsm.outputs(o_names) 23 | 24 | def __str__(self): 25 | s = [] 26 | s.append('state(%s)' % self.s) 27 | s.append('input(%s)' % self.i) 28 | s.append('output(%s)' % self.o) 29 | return ' '.join(s) 30 | 31 | def fsm(self, sv, iv): 32 | self.s.set_sv(sv) 33 | self.i.set_iv(iv) 34 | 35 | # defaults - everything off, no state change 36 | self.o.set_null() 37 | new_sv = sv 38 | 39 | # outputs 40 | if self.s.in_state('fill_sj'): 41 | self.o.set('sj_pump') 42 | 43 | if self.s.in_state('fill_well'): 44 | self.o.set('well_pump') 45 | self.o.set('chlorinator') 46 | 47 | # state changes 48 | if self.i.is_set('stop'): 49 | # stop has been pressed 50 | new_sv = self.s.state('stopped') 51 | 52 | elif self.s.in_state('full'): 53 | if self.i.is_set('not_full'): 54 | # the tank is not full 55 | if self.i.is_set('start_well'): 56 | # manual well switch -> fill from well 57 | new_sv = self.s.state('fill_well') 58 | elif self.i.is_set('start_sj'): 59 | # manual sj switch -> fill from sj 60 | new_sv = self.s.state('fill_sj') 61 | 62 | elif self.s.in_state('fill_sj'): 63 | if self.i.is_clr('not_full'): 64 | # the tank is full 65 | new_sv = self.s.state('full') 66 | elif self.i.is_set('start_well'): 67 | # manual well switch -> fill from well 68 | new_sv = self.s.state('fill_well') 69 | 70 | elif self.s.in_state('fill_well'): 71 | if self.i.is_clr('not_full'): 72 | # the tank is full 73 | new_sv = self.s.state('full') 74 | elif self.i.is_set('start_sj'): 75 | # manual sj switch -> fill from sj 76 | new_sv = self.s.state('fill_sj') 77 | 78 | elif self.s.in_state('stopped'): 79 | if self.i.is_clr('stop'): 80 | # the tank is full 81 | new_sv = self.s.state('full') 82 | 83 | return new_sv, self.o.ov 84 | 85 | #------------------------------------------------------------------------------ 86 | --------------------------------------------------------------------------------