Re: [myhdl-list] Python Hardware Processor
Brought to you by:
jandecaluwe
From: Christopher F. <chr...@gm...> - 2012-02-04 22:12:20
|
Norbo, If you like you can create a project page on the MyHDL wiki space. Or you might want to share the code via BitBucket or GitHub. Regards, Chris On 2/4/12 7:48 AM, Norbo wrote: > To show where i am at: > ##################################### > # File: Core.py > # Author: Norbert Feurle > # Date: 4.2.2012 > # > # Description: Python Hardware Processor > # This Code implements a Hardware CPU in myhdl. The code for the cpu is > directly written in python > # and since the hardware description is also in python, the programmcode > is automatically into the design > # The cool thing is that by this approach only memory and stack hardware > is allocated in Hardware which is > # really needed. Simulation of the code on the cpu running is no problem. > (only python needed!) > # The bytecode of python is executed at the cpu at one instruction per > cycle > # By now only simple bytecode instructions are supported > # You can simply instantiate more Hardware CPUs on an FPGA with different > program code to make it run parallel > # > # License: Pro life licence. > # You are not allowed to this code for any purpose which is in any kind > disrespectfully to life and/or freedom > # In such a case it remains my property > # for example: You are not allowed to build weapons, or large-scale > animal husbandry automation equipment or anything > # that is used to harm, destroy or is used to frighten living beings at > an unacceptable rate, or build hardware > # with it where te workers dont get paid appropriate, or any process > where the waste issue isnt solved in a sustainable way, # etc.. > # > # If you use it respectfully you are allowed to use it for free and are > welcome to contribute with your changes/extensions > ################################################################################################# > > > > ######################################################################################## > #### Limitations: > #### no lists, no dicts no tuples, no float, no complex numbers, > #### no classmembers,no function calls, no multiplication, no exeptions, > no for x in xxx:, etc. > ######################################################################################## > ### What is supported: > ### ints(with x bits), no other python types > ### simple if and while and assignments > ### Operators: +, ^ , |,&. - (minus doesent work yet becaus i never used > signed (big TODO)) > ### Simple Digital IO with PORTs > ### And of corse some bugs > ######################################################################################## > > ##### The main programm for the python hardware porcessor to execute #### > def CPU_main(): > ## Reserves for IO Module ### > global PORTA_IN,PORTB_IN,PORTC_OUT,PORTD_OUT > PORTA_IN=0 #needs to bee here to reserve the right addresses (0 is > written to nowhere) > PORTB_IN=0 > PORTC_OUT=0 > PORTD_OUT=0 > ################# > x=0 > while 1: > x=x+1 > if PORTA_IN==1: > PORTC_OUT= PORTC_OUT^1 > if x<20: > PORTD_OUT=x > elif x>=20 and x<25: > PORTD_OUT=2**30+x > else: > PORTD_OUT=x > ##### End of the main programm #### > > > from myhdl import * > import math > import dis > HAVE_ARGUMENT=dis.HAVE_ARGUMENT > GLOBAL_PROGRAM=tuple([ord(i) for i in CPU_main.func_code.co_code]) + > (0,0,) #because arguments are also read > GLOBAL_CONSTANTS=(0,)+CPU_main.func_code.co_consts[1:] # because constant > None is on address 0 but cpu only supports int > GLOBAL_STACK_SIZE=CPU_main.func_code.co_stacksize > > GLOBAL_NUMBERSTACK_OPS=19 > STACK_NOP,STACK_ADD,STACK_POSITIVE,STACK_NOT,STACK_NEGATIVE,STACK_INVERT,STACK_RSHIFT,STACK_LSHIFT,STACK_AND,STACK_SUB,STACK_OR,STACK_XOR,STACK_POP,STACK_LOAD,STACK_CMP,STACK_ROT_FOUR, > STACK_ROT_TWO, STACK_ROT_THREE,STACK_DUP_TOP=range(GLOBAL_NUMBERSTACK_OPS) > > > def RAM(dout, din, addr, we, clk, WORD_SZ=8, DEPTH=16384): > """ > > """ > mem = [Signal(intbv(0)[WORD_SZ:]) for i in range(DEPTH)] > > @always(clk.posedge) > def write(): > if we: > mem[int(addr)].next = din > > @always_comb > def read(): > dout.next = mem[int(addr)] > > return write,read > > > def ROM(dout,addr,CONTENT): > @always_comb > def rom_logic(): > dout.next= CONTENT[int(addr)] > > return rom_logic > > def ProgrammROM(Opcode,Arg1,Arg2,addr,CONTENT): > @always_comb > def progrom_logic(): > Opcode.next= CONTENT[int(addr)] > Arg1.next= CONTENT[int(addr+1)] > Arg2.next= CONTENT[int(addr+2)] > > return progrom_logic > > def > Stack(clk,rst,TopData_Out,Data_In,StackOP,CMPmode,WORD_SZ=32,SIZE=GLOBAL_STACK_SIZE): > Stack_mem = [Signal(intbv(0)[WORD_SZ:]) for i in range(SIZE)] > > Next_TSO_Data=Signal(intbv(0)[WORD_SZ:]) > TOS_Data=Signal(intbv(0)[WORD_SZ:]) > TOS1_Data=Signal(intbv(0)[WORD_SZ:]) > TOS2_Data=Signal(intbv(0)[WORD_SZ:]) > > TOS_Data_RD=Signal(intbv(0)[WORD_SZ:]) > TOS1_Data_RD=Signal(intbv(0)[WORD_SZ:]) > TOS2_Data_RD=Signal(intbv(0)[WORD_SZ:]) > > TOS_pointer_next=Signal(intbv(0,min=0,max=SIZE)) > TOS_pointer= Signal(intbv(0,min=0,max=SIZE)) > TOS1_pointer= Signal(intbv(0,min=0,max=SIZE)) > TOS2_pointer= Signal(intbv(0,min=0,max=SIZE)) > #TOS3_pointer= Signal(intbv(0,min=0,max=SIZE)) > > enable_stackpointer_increase=Signal(bool(0)) > enable_stackpointer_deacrease=Signal(bool(0)) > > @always(clk.posedge,rst.negedge) > def seq_logic(): > if rst == 0: > TOS_pointer_next.next=0 > TOS_pointer.next=0 > TOS1_pointer.next=0 > TOS2_pointer.next=0 > #TOS3_pointer.next=0 > for indexer in range(SIZE): > Stack_mem[int(indexer)].next=0 > else: > if enable_stackpointer_increase: > TOS_pointer_next.next=(TOS_pointer_next+1)% SIZE > TOS_pointer.next=TOS_pointer_next > TOS1_pointer.next=TOS_pointer > TOS2_pointer.next=TOS1_pointer > if enable_stackpointer_deacrease: > if TOS2_pointer==0: > TOS2_pointer.next=SIZE-1 > else: > TOS2_pointer.next=TOS2_pointer-1 > > TOS1_pointer.next=TOS2_pointer > TOS_pointer.next=TOS1_pointer > TOS_pointer_next.next=TOS_pointer > #Stackpointer.next=Stackpointer_next > > #attention if stacksize is to smalll the lower one gets overwritten > #so the ordering is important > # TODO: stacksize==1 would not work > Stack_mem[int(TOS2_pointer)].next=TOS2_Data > Stack_mem[int(TOS1_pointer)].next=TOS1_Data > Stack_mem[int(TOS_pointer)].next=TOS_Data > Stack_mem[int(TOS_pointer_next)].next=Next_TSO_Data > > > > @always_comb > def comb_logic(): > Next_TSO_Data.next=Stack_mem[int(TOS_pointer_next)] > TOS_Data.next=Stack_mem[int(TOS_pointer)] > TOS1_Data.next=Stack_mem[int(TOS1_pointer)] > TOS2_Data.next=Stack_mem[int(TOS2_pointer)] > > enable_stackpointer_increase.next=1 > enable_stackpointer_deacrease.next=0 > > if StackOP==STACK_NOP: > enable_stackpointer_increase.next=0 > elif StackOP==STACK_ADD: > Next_TSO_Data.next=TOS1_Data_RD+TOS_Data_RD > elif StackOP==STACK_POSITIVE: #??? > Next_TSO_Data.next=TOS_Data_RD > elif StackOP==STACK_NOT: > Next_TSO_Data.next=TOS_Data_RD > elif StackOP==STACK_NEGATIVE: > Next_TSO_Data.next=-TOS_Data_RD > elif StackOP==STACK_INVERT: > Next_TSO_Data.next=~TOS_Data_RD > elif StackOP==STACK_RSHIFT: > Next_TSO_Data.next=TOS1_Data_RD>>TOS_Data_RD > elif StackOP==STACK_LSHIFT: > Next_TSO_Data.next=TOS1_Data_RD<<TOS_Data_RD > elif StackOP==STACK_AND: > Next_TSO_Data.next=TOS1_Data_RD&TOS_Data_RD > elif StackOP==STACK_SUB: > Next_TSO_Data.next=TOS1_Data_RD-TOS_Data_RD > elif StackOP==STACK_OR: > Next_TSO_Data.next=TOS1_Data_RD|TOS_Data_RD > elif StackOP==STACK_XOR: > Next_TSO_Data.next=TOS1_Data_RD^TOS_Data_RD > elif StackOP==STACK_POP: > enable_stackpointer_increase.next=0 > enable_stackpointer_deacrease.next=1 > elif StackOP==STACK_LOAD: > Next_TSO_Data.next=Data_In > > elif StackOP==STACK_ROT_TWO: > enable_stackpointer_increase.next=0 > TOS_Data.next=TOS1_Data_RD > TOS1_Data.next=TOS_Data_RD > elif StackOP==STACK_ROT_THREE: > enable_stackpointer_increase.next=0 > TOS_Data.next=TOS1_Data_RD > TOS1_Data.next=TOS2_Data_RD > TOS2_Data.next=TOS_Data_RD > #elif StackOP==STACK_ROT_FOUR: ##TODO > #TOS_Data.next=Stack_mem[int(TOS_pointer)] > # TOS1_Data.next=Stack_mem[int(TOS1_pointer)] > # TOS2_Data.next=Stack_mem[int(TOS2_pointer)] > elif StackOP==STACK_DUP_TOP: > Next_TSO_Data.next=TOS_Data_RD > elif StackOP==STACK_CMP: > if CMPmode==0: #operator< > if TOS1_Data_RD<TOS_Data_RD: > Next_TSO_Data.next=1 > else: > Next_TSO_Data.next=0 > if CMPmode==1: #operator<= > if TOS1_Data_RD<=TOS_Data_RD: > Next_TSO_Data.next=1 > else: > Next_TSO_Data.next=0 > if CMPmode==2: #operator == > if TOS1_Data_RD==TOS_Data_RD: > Next_TSO_Data.next=1 > else: > Next_TSO_Data.next=0 > if CMPmode==3: #operator != > if TOS1_Data_RD!=TOS_Data_RD: > Next_TSO_Data.next=1 > else: > Next_TSO_Data.next=0 > if CMPmode==4: #operator> > if TOS1_Data_RD>TOS_Data_RD: > Next_TSO_Data.next=1 > else: > Next_TSO_Data.next=0 > if CMPmode==5: #operator>= > if TOS1_Data_RD>=TOS_Data_RD: > Next_TSO_Data.next=1 > else: > Next_TSO_Data.next=0 > else: > enable_stackpointer_increase.next=0 > > > > @always_comb > def comb_logic2(): > TOS_Data_RD.next=Stack_mem[int(TOS_pointer)] > TOS1_Data_RD.next=Stack_mem[int(TOS1_pointer)] > TOS2_Data_RD.next=Stack_mem[int(TOS2_pointer)] > TopData_Out.next=Stack_mem[int(TOS_pointer)] > > return seq_logic,comb_logic,comb_logic2 > > def > IOModule(clk,rst,dout,din,addr,we,PORTA_IN,PORTB_IN,PORTC_OUT,PORTD_OUT,WIDTH=32): > Sync_in1_PORTA=Signal(intbv(0)[WIDTH:]) > Sync_in2_PORTA=Signal(intbv(0)[WIDTH:]) > Sync_in1_PORTB=Signal(intbv(0)[WIDTH:]) > Sync_in2_PORTB=Signal(intbv(0)[WIDTH:]) > > INTERN_PORTC_OUT=Signal(intbv(0)[WIDTH:]) > INTERN_PORTD_OUT=Signal(intbv(0)[WIDTH:]) > @always(clk.posedge,rst.negedge) > def IO_write_sync(): > if rst==0: > INTERN_PORTC_OUT.next=0 > INTERN_PORTD_OUT.next=0 > else: > Sync_in1_PORTA.next=PORTA_IN > Sync_in2_PORTA.next=Sync_in1_PORTA > > Sync_in1_PORTB.next=PORTB_IN > Sync_in2_PORTB.next=Sync_in1_PORTB > > if we: > if addr==2: > INTERN_PORTC_OUT.next=din > elif addr==3: > INTERN_PORTD_OUT.next=din > > > @always_comb > def IO_read(): > PORTC_OUT.next=INTERN_PORTC_OUT > PORTD_OUT.next=INTERN_PORTD_OUT > dout.next = 0 > if addr==0: > dout.next = Sync_in2_PORTA > if addr==1: > dout.next = Sync_in2_PORTB > if addr==2: > dout.next = INTERN_PORTC_OUT > if addr==3: > dout.next = INTERN_PORTD_OUT > > return IO_write_sync,IO_read > > > def > Processor(clk,rst,PORTA_IN,PORTB_IN,PORTC_OUT,PORTD_OUT,CPU_PROGRAM=GLOBAL_PROGRAM,CPU_CONSTANTS=GLOBAL_CONSTANTS,VAR_BITWIDTH=32,VAR_DEPTH=20,STACK_SIZE=GLOBAL_STACK_SIZE): > > > #### helping signals > EnableJump=Signal(bool(0)) > JumpValue=Signal(intbv(0)[8:]) > > #### Programm Memory Signals > Opcode=Signal(intbv(0)[8:]) > Arg1=Signal(intbv(0)[8:]) > Arg2=Signal(intbv(0)[8:]) > ProgramCounter=Signal(intbv(0,min=0,max=len(CPU_main.func_code.co_code))) > > #### Constants Memory Signals > ConstantsData=Signal(intbv(0)[VAR_BITWIDTH:]) > ConstantsAddr=Signal(intbv(0,min=0,max=len(CPU_CONSTANTS))) > > #### Programm Variables RAM Signals > Varibles_DataOut=Signal(intbv(0)[VAR_BITWIDTH:]) > Variables_DataIn=Signal(intbv(0)[VAR_BITWIDTH:]) > VariablesAddr=Signal(intbv(0,min=0,max=VAR_DEPTH)) > Variables_we=Signal(bool(0)) > > #### Stack Signals > # STACK_SIZE > Stack_DataIn=Signal(intbv(0)[VAR_BITWIDTH:]) > StackValue0=Signal(intbv(0)[VAR_BITWIDTH:]) > StackOP=Signal(intbv(0,min=0,max=GLOBAL_NUMBERSTACK_OPS)) > StackOP_CMPmode=Signal(intbv(0,min=0,max=len(dis.cmp_op))) > > #### IO Module Signals > IO_MODULE_STARTADDRESSES=4 > #IO_MODULE_ADDRESBITS=int(math.log(IO_MODULE_STARTADDRESSES,2)+1)-1 > IO_DataOut=Signal(intbv(0)[VAR_BITWIDTH:]) > IO_DataIn=Signal(intbv(0)[VAR_BITWIDTH:]) > IO_addr=Signal(intbv(0,min=0,max=IO_MODULE_STARTADDRESSES)) > IO_we=Signal(bool(0)) > > ####Variables RAM instantiation > VariablesRAM_inst=RAM(Varibles_DataOut, Variables_DataIn, > VariablesAddr, Variables_we, clk, WORD_SZ=VAR_BITWIDTH, DEPTH=VAR_DEPTH) > > ###Programm Code Memory instantiation > ProgrammCode_inst=ProgrammROM(Opcode,Arg1,Arg2,ProgramCounter,CPU_PROGRAM) > > ###Constants memory instantiation > ConstantsROM_inst=ROM(ConstantsData,ConstantsAddr,CPU_CONSTANTS) > > ###The stack > TheStack_inst=Stack(clk,rst,StackValue0,Stack_DataIn,StackOP,StackOP_CMPmode, > WORD_SZ=VAR_BITWIDTH,SIZE=STACK_SIZE) > > ###I/O Module instantiation > IOModule_inst=IOModule(clk,rst,IO_DataOut,IO_DataIn,IO_addr,IO_we,PORTA_IN,PORTB_IN,PORTC_OUT,PORTD_OUT,WIDTH=VAR_BITWIDTH) > > @always(clk.posedge,rst.negedge) > def seq_logic(): > if rst == 0: > ##### ProgramCounter Part ######## > ProgramCounter.next = 0 > ##### END ProgramCounter Part ######## > > else: > ##### ProgramCounter Part ######## > if > EnableJump==True:#StackValue0==bool(1)#Opcode==dis.opmap['POP_JUMP_IF_FALSE'] > or Opcode==dis.opmap['POP_JUMP_IF_TRUE'] > ProgramCounter.next=JumpValue > else: > if Opcode>= HAVE_ARGUMENT: > ProgramCounter.next = ProgramCounter+3 > else: > ProgramCounter.next = ProgramCounter+1 > ##### END ProgramCounter Part ######## > > ##### Opcode handling############# > @always_comb > def comb_logic(): > > JumpValue.next=0 > EnableJump.next=False > > ConstantsAddr.next=0 > > StackOP.next=STACK_NOP > StackOP_CMPmode.next=0 > Stack_DataIn.next=0 > > Variables_we.next=False > VariablesAddr.next=0 > Variables_DataIn.next=StackValue0 > > IO_we.next=False > IO_addr.next=0 > IO_DataIn.next=StackValue0 > > if Opcode==23: #dis.opmap['BINARY_ADD']: # 23, > StackOP.next=STACK_ADD > elif Opcode==64: #dis.opmap['BINARY_AND']: # 64, > StackOP.next=STACK_ADD > elif Opcode==62: #dis.opmap['BINARY_LSHIFT']: #: 62, > StackOP.next=STACK_LSHIFT > elif Opcode==66: #dis.opmap['BINARY_OR']: #: 66, > StackOP.next=STACK_OR > elif Opcode==63: #dis.opmap['BINARY_RSHIFT']: #: 63, > StackOP.next=STACK_RSHIFT > elif Opcode==24: #dis.opmap['BINARY_SUBTRACT']: #: 24, > StackOP.next=STACK_SUB > elif Opcode==65: #dis.opmap['BINARY_XOR']: #: 65, > StackOP.next=STACK_XOR > elif Opcode==107: #dis.opmap['COMPARE_OP']: #: 107, > StackOP.next=STACK_CMP > StackOP_CMPmode.next=Arg1 > elif Opcode==4: #dis.opmap[''DUP_TOP']: # 4 > StackOP.next=STACK_DUP_TOP > elif Opcode==113: #dis.opmap['JUMP_ABSOLUTE'] : #: 113, > EnableJump.next=True > JumpValue.next=Arg1 > elif Opcode==110: #dis.opmap['JUMP_FORWARD']: #: 110, > EnableJump.next=True > JumpValue.next=ProgramCounter+3+Arg1 > elif Opcode==111: #dis.opmap['JUMP_IF_FALSE_OR_POP']: #: 111, > if StackValue0==0: > EnableJump.next=True > JumpValue.next=Arg1 > elif Opcode==112: #dis.opmap['JUMP_IF_TRUE_OR_POP']: #: 112, > if StackValue0==1: > JumpValue.next=Arg1 > EnableJump.next=True > else: > StackOP.next=STACK_POP > elif Opcode==100: #dis.opmap['LOAD_CONST']: #: 100, > StackOP.next=STACK_LOAD > Stack_DataIn.next=ConstantsData > ConstantsAddr.next=Arg1 > elif Opcode==124: #dis.opmap['LOAD_FAST']: #: 124, > StackOP.next=STACK_LOAD > VariablesAddr.next=Arg1 > Stack_DataIn.next=Varibles_DataOut > elif Opcode==116: #dis.opmap['LOAD_GLOBAL']: 116, > StackOP.next=STACK_LOAD > IO_addr.next=Arg1 > Stack_DataIn.next=IO_DataOut > elif Opcode==97: #dis.opmap[''STORE_GLOBAL']: 97, > IO_we.next=True > IO_addr.next=Arg1 > elif Opcode==114: #dis.opmap['POP_JUMP_IF_FALSE']: #: 114, > StackOP.next=STACK_POP > if StackValue0==0: > JumpValue.next=Arg1 > EnableJump.next=True > elif Opcode==115: #dis.opmap['POP_JUMP_IF_TRUE']: #: 115, > StackOP.next=STACK_POP > if StackValue0==1: > JumpValue.next=Arg1 > EnableJump.next=True > elif Opcode==1: #dis.opmap['POP_TOP'] > StackOP.next=STACK_POP > elif Opcode==5: #dis.opmap['ROT_FOUR']: #: 5, > StackOP.next=STACK_ROT_FOUR > elif Opcode==3: #dis.opmap['ROT_THREE']: #: 3, > StackOP.next=STACK_ROT_THREE > elif Opcode==2: #dis.opmap['ROT_TWO']: #: 2, > StackOP.next=STACK_ROT_TWO > elif Opcode==125: #dis.opmap['STORE_FAST']: #: 125, > Variables_we.next=True > VariablesAddr.next=Arg1 > elif Opcode==15: #dis.opmap['UNARY_INVERT']: #:15 > StackOP.next=STACK_INVERT > elif Opcode==11: #dis.opmap['UNARY_NEGATIVE']: #: 11, > StackOP.next=STACK_NEGATIVE > elif Opcode==12: #dis.opmap['UNARY_NOT']: #: 12, > StackOP.next=STACK_NOT > elif Opcode==10: #dis.opmap['UNARY_POSITIVE']: #: 10, > StackOP.next=STACK_POSITIVE > elif Opcode==120: #dis.opmap['SETUP_LOOP']: # TODO?? > StackOP.next=STACK_NOP > else: > StackOP.next=STACK_NOP > #raise ValueError("Unsuported Command:"+str(Opcode)) > print "Comand not supported:",Opcode > > return > seq_logic,comb_logic,VariablesRAM_inst,ProgrammCode_inst,ConstantsROM_inst,TheStack_inst,IOModule_inst > > > def convert(): > WORD_SZ=8 > DEPTH=16384 > > we, clk = [Signal(bool(0)) for i in range(2)] > dout = Signal(intbv(0)[WORD_SZ:]) > din = Signal(intbv(0)[WORD_SZ:]) > addr = Signal(intbv(0)[16:]) > > toVHDL(RAM, dout, din, addr, we,clk,WORD_SZ,DEPTH) > > > def Processor_TESTBENCH(): > rst, clk = [Signal(bool(0)) for i in range(2)] > PORTA_IN=Signal(intbv(0)[32:]) > PORTB_IN=Signal(intbv(0)[32:]) > PORTC_OUT=Signal(intbv(0)[32:]) > PORTD_OUT=Signal(intbv(0)[32:]) > > toVHDL(Processor,clk,rst,PORTA_IN,PORTB_IN,PORTC_OUT,PORTD_OUT) > Processor_inst=Processor(clk,rst,PORTA_IN,PORTB_IN,PORTC_OUT,PORTD_OUT) > > > > @always(delay(10)) > def clkgen(): > clk.next = not clk > > @instance > def stimulus(): > print "#"*10+"Reseting"+"#"*10 > rst.next=0 > print "#"*10+"Setting PORTA_IN too 0"+"#"*10 > PORTA_IN.next=0 > > for i in range(3): > yield clk.negedge > print "#"*10+"Release Reset"+"#"*10 > rst.next=1 > > for i in range(200): > yield clk.negedge > > print "#"*10+"Setting PORTA_IN too 1"+"#"*10 > PORTA_IN.next=1 > for i in range(200): > yield clk.negedge > > PORTA_IN.next=0 > for i in range(500): > yield clk.negedge > > raise StopSimulation > > @instance > def Monitor_PORTC(): > print "\t\tPortC:",PORTC_OUT > while 1: > yield PORTC_OUT > print "\t\tPortC:",PORTC_OUT > > @instance > def Monitor_PORTD(): > print "PortD:",PORTD_OUT > while 1: > yield PORTD_OUT > print "PortD:",PORTD_OUT > > return clkgen,Processor_inst,stimulus,Monitor_PORTC,Monitor_PORTD > > #tb = traceSignals(Processor_TESTBENCH) > sim = Simulation(Processor_TESTBENCH()) > sim.run() > > #convert() > > #def simulate(timesteps): > # tb = traceSignals(test_dffa) > # sim = Simulation(tb) > # sim.run(timesteps) > > #simulate(20000) > > > ------------------------------------------------------------------------------ > Try before you buy = See our experts in action! > The most comprehensive online learning library for Microsoft developers > is just $99.99! Visual Studio, SharePoint, SQL - plus HTML5, CSS3, MVC3, > Metro Style Apps, more. Free future releases when you subscribe now! > http://p.sf.net/sfu/learndevnow-dev2 |