#include // global variables available anywhere const byte addr = 0x3C; // SH1106 display I2C address const byte RE1chA = 2; // Rotary Encoder 1 (timebase) channel A (interrupt 0 on pin2) const byte RE1chB = 4; // Rotary Encoder 1 channel B (pin4) const byte RE2chA = 3; // Rotary Encoder 2 (trigger) channel A (interrupt 1 on pin3) const byte RE2chB = 5; // Rotary Encoder 2 channel B (pin5) const byte RE2sw = 6; // Rotary Encoder 2 switch (pin6) const byte Menubutton = 7; // Menu button (pin7) const byte modecycle[] = {15,19,19,24,24,24,30,30,30}; // 2,5,10,25,50,100,250,500,1000 uS const byte tbval[] = {196,162,146,140,0,78,138,138,114,0,124,130,124,0,124,130,124,4,254,0,124,130,124, 0,124,130,124,4,254,0,136,148,100,0,126,2,124,2,126,0,68,74,50}; // Timebase character bitmaps (25001001sms) const byte tbpos[2][9] = {0,5,17,0,5,17,0,5,27,4,4,6,9,8,10,13,12,7}; // Pointer for tbval position & length const byte clsframe[8][3] = {0xFE,0x02,0xFE,0xFF,0,0xFF,0xFF,0,0xFF,0xFF,0,0xFF,0xFF,0,0xFF,0xFF,0,0xFF, 0xFF,0,0xFF,0x7f,0x40,0x7F}; // Bit patterns for clearscreen frame const byte titles[] = {62,65,65,62,0,68,74,50,0,60,66,66,0,125,0,127,0,127,0,60,66,60,0,68,74,50,0,60,66, 66,0,60,66,60,0,254,18,28,0,126,82,92}; const byte menulabels[] = {24,0xB2,0x11,0x02,127,9,25,102,0,58,74,126,0,126,2,2,124,0,156,162,254,0,126, 82,92,36,0xB4,0x11,0x02,1,1,127,1,1,0,125,0,126,2,124,2,126,0,126,82,92,0,127,68,56,0,58,74, 126,0,68,74,50,0,126,82,92,29,0xB6,0x11,0x02,1,1,127,1,1,124,2,2,0,125,0,156,162,254,0,156, 162,254,0,126,82,92,0,124,2,2}; // Length, Page, ColH ColL data for Range/Timebase/Trigger const byte labelpt[] = {0,25,62}; // Pointer for menulabels start positions const byte rangelabels[] = {39,69,69,57,0,14,48,64,48,14,0,56,68,127,0,60,66,36}; // 5vdc const byte triglabels[] = {124,2,2,0,125,0,68,74,50,0,126,82,92,0,0,126,9,1,0,58,74,126,0,127,0,127,0,0, 126,2,124,2,126,0,58,74,126,0,126,2,2,124,126,9,1,124,2,2,0,126,82,92,0,126,82,92}; // rise/fall,man,free const byte limitlabel[] = {24,60,126,255,255,255}; // Bit pattern for option selection bound markers unsigned long cyctim; // ADC cycle timer byte Aval[135][2]; // Array to hold ADC readings: 0 = bar top, 1 = bar bottom in the range 0-63 byte Block[16][8]; // Buffer array to display 16 columns x 8 pages byte Menuopt = 1; // 0 = Function, 1 = Range, 2 = Timebase, 3 = Trigger byte optlimits = B11000100; // Menu option limit indicator/ B0,4 L&R limits for Menuopt 0, etc. 0 = limit reached byte tb = 1; // Timebase mode selection (range 0-8) byte cas; // Current ADC Sample counter byte cds; // Current Display Sample counter byte trigmode = 0; // Trigger Mode: 0 = auto(+), 1 = auto (-), 2 = manual, 3 = freerun byte trigval = 104; // Trigger level value (range 0-247) byte trigfall = 0; // Set default trigger mode to rising (0), falling = FF byte trigger; // Trigger status: 0 = Not triggered, 1 = auto pre trigger, 2 = auto trigger, 3 = manual trigger byte trigblk = 4; // page for trigger block byte trigbit = 56; // bits for trigger block byte trigb2 = 0; // overflow bits for trigger block bool RE1int = false; // Rotary Encoder 1 interrupt flag bool RE1dirn; // Rotary Encoder 1 direction flag (0 = clockwise) bool RE2int = false; // Rotary Encoder 2 interrupt flag bool RE2dirn; // Rotary Encoder 2 direction flag (0 = clockwise) bool menuint; // Menu button "interrupt" flag void setup() { // ------------------------------ Setup ------------------------------------ // ADC set up ADCSRA = 0x94; // divider 128 => 125kHz => 9600sps (16MHz Uno clock, 13 cycles per conversion) ADMUX = 0x60; // AVCC ref voltage, Left justified result, ADC0 ADCSRB = 0x0; // Analog Comparator Mux disabled ACSR = 0x10; // ACI cleared // Display set up Wire.begin(); // join i2c bus as master TWBR=5; // set clock: 12 = 400kHz, 5 = 615kHz (fastest) // SH1106 datasheet at www.velleman.eu/downloads/29/infosheets/sh1106_datasheet.pdf Wire.beginTransmission(addr); // Set up SH1106 display Wire.write(0x00); // Control: 00 = Command data to follow. 80 = 1 Command then another Control Wire.write(0xAE); // Command: AEH = Display off, AFH = Display on Wire.write(0x40); // Command: Set display start line 40H-7FH = line 0 - line 63 (for scrolling) Wire.write(0xA1); // Command: Set column addressing direction A0H = L-R, A1H = R-L Wire.write(0xC8); // Command: Set line addressing direction C0H = Top - bottom, C8H = bottom to top Wire.write(0x02); // Command: Set col addr low bits 00H-0FH => column 0-15 Wire.write(0xAF); // Command: Display on Wire.endTransmission(); // Rotary Encoders and buttons set up pinMode (RE1chA,INPUT_PULLUP); // Channel A - goes low on movement pinMode (RE1chB,INPUT_PULLUP); // Channel B - indicates direction pinMode (RE2chA,INPUT_PULLUP); // Channel A - goes low on movement pinMode (RE2chB,INPUT_PULLUP); // Channel B - indicates direction pinMode (RE2sw,INPUT_PULLUP); // Switch - low when pressed pinMode (Menubutton,INPUT_PULLUP); // Switch - low when pressed attachInterrupt(digitalPinToInterrupt(RE1chA), RE1move, FALLING); // Interrupt 0 on Rotary Encoder 1 Ch A falling edge attachInterrupt(digitalPinToInterrupt(RE2chA), RE2move, FALLING); // Interrupt 1 on Rotary Encoder 2 Ch A falling edge } void loop() { // ----------------------------- Main loop ----------------------------------- while(!digitalRead(Menubutton)) { delay(5); } // Ensure last Menu button press is released menuint = false; // Clear Menu button press flag menucls(0xFF); // Clear display and add menu frame titledisp(); byte x=0; // Menu option counter for(x=0;x<=2;x++) { dispmenu(labelpt[x]); } // Add option labels rangemenu(); // Add current range option tbmenu(); // Add current timebase option trigmenu(); // Add current trigger option dispactive(0xF0); // Add current active option markers RE1int = false; // Clear any outstanding interrupts from sweep mode ... RE2int = false; // as the rotary encoders have different meanings in the menu screen while(digitalRead(Menubutton)) { // Loop unless Menu button is pressed to start oscilloscope if(RE1int) { if(Menuopt==1) { // Range - No action required as only 1 Range option available initially while(!digitalRead(RE1chA)) { } // Wait till Rotary Encoder ch A goes low again before resetting interrupt flag RE1int = false; // Reset interrupt flag } if(Menuopt==2) { // Timebase timebaseset(); tbmenu(); dispactive(0xF0); } if(Menuopt==3) { // Trigger mode trigmodeset(); trigmenu(); dispactive(0xF0); } } if(RE2int) { Menuoptset(); } } bitSet(ADCSRA, ADSC); // "Warm up" the ADC while(!digitalRead(Menubutton)) { delay(5); } // Wait for Menu button to be released while(!menuint) { // Continue sample/display cycle until Menu button is pressed disprefresh(); timebaselabel(); if(RE1int) { timebaseset(); } if(RE2int) { triggerset(); } oscsweep(); } } void menucls(byte frame) { // ---------------------- Clear display for menu ---------------------------- // frame = 0 for clear screen, FF for frame around screen byte i,j; for(j=0; j<=7; j++) { Wire.beginTransmission(addr); Wire.write(0x00); // Control: Command data to follow Wire.write(j+176); // Command: Set page addr B0H-B7H = Page 0 - Page 7 Wire.write(0x10); // Command: Set col addr high bits 10H-1FH => 0n-Fn Wire.write(0x02); // Command: Set col addr low bits 00H-0FH => column 0-15 Wire.endTransmission(); Wire.beginTransmission(addr); Wire.write(0x40); // Control: Data bytes data to follow Wire.write(0); // first column blank Wire.write(clsframe[j][0]&frame); // then frame if applicable for(i=4; i<=127; i++) { Wire.write(clsframe[j][1]&frame); if(!(i%30)) { // Reset buffer after 30 bytes Wire.endTransmission(); Wire.beginTransmission(addr); Wire.write(0x40); // Control: Data bytes data to follow } } Wire.write(clsframe[j][2]&frame); Wire.write(0); Wire.endTransmission(); } } void dispmenu(byte posn) { // ---------------------- Display menu items ---------------------------- byte i; byte len = menulabels[posn]; Wire.beginTransmission(addr); for(i=1; i<=3; i++) { Wire.write(0x80); // Control: 1 Command data to follow Wire.write(menulabels[posn+i]); } Wire.write(0x40); // Control: Data bytes data to follow while(i<=len) { Wire.write(menulabels[posn+i]); i++; if(!(i%26)) { Wire.endTransmission(); Wire.beginTransmission(addr); Wire.write(0x40); // Control: Data bytes data to follow } } Wire.endTransmission(); } void dispactive(int limits) { // -------------------- Display active item markers -------------------------- // limits = 0 clears previouslimits, F0 = write new limits byte i; if(limits) { // use optlimits bit pattern to set masks in limits if(bitRead(optlimits,Menuopt)) { limits |= 0xF; } // set bits 0-3 for lower limit not reached if(bitRead(optlimits,Menuopt+4)) { limits |= 0xF00; } // set bits 8-11 for upper limits not reached } Wire.beginTransmission(addr); Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0xB0+2*Menuopt); // Command: Set page addr B0H-B7H = Page 0 - Page 7 Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x14); // Command: Set col addr high bits 10H-1FH => 0n-Fn Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x0A); //Command: Set col addr low bits 00H-0FH => column 0-15 Wire.write(0x40); // Control: Data bytes data to follow for(i=0;i<=5;i++) { if(bitRead(limits,i)) { Wire.write(limitlabel[i]); } else { Wire.write(0); } // Apply mask to lower markers } Wire.endTransmission(); Wire.beginTransmission(addr); Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x16); // Command: Set col addr high bits 10H-1FH => 0n-Fn Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x0C); //Command: Set col addr low bits 00H-0FH => column 0-15 Wire.write(0x40); // Control: Data bytes data to follow for(i=6;i<=11;i++) { if(bitRead(limits,i)) { Wire.write(limitlabel[11-i]); } else { Wire.write(0); } // Apply mask to upper markers } Wire.endTransmission(); } void titledisp() { // ----------------------- Function title display ----------------------------- Wire.beginTransmission(addr); Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0xB0); // Command: Set page addr B0H = Page 0 Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x12); // Command: Set col addr high bits 10H-1FH => 0n-Fn Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x03); // Command: Set col addr low bits 0H-0FH => 0n-Fn Wire.write(0x40); // Control: Data bytes data to follow for(byte i=0;i<62;i++) { if(i<10 | i>51) { Wire.write(0);} else { Wire.write(titles[i-10]); } if(!(i%22)) { // Reset buffer before 30 bytes Wire.endTransmission(); Wire.beginTransmission(addr); Wire.write(0x40); // Control: Data bytes data to follow } } Wire.endTransmission(); } void rangemenu() { // ----------------------- Menu display range options ----------------------------- Wire.beginTransmission(addr); Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0xB2); // Command: Set page addr B2H = Page 2 Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x15); // Command: Set col addr high bits 10H-1FH => 0n-Fn Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x05); // Command: Set col addr low bits 0H-0FH => 0n-Fn Wire.write(0x40); // Control: Data bytes data to follow for(byte i=0;i<=17;i++) { Wire.write(rangelabels[i]); } Wire.endTransmission(); } void tbmenu() { // ----------------------- Menu display timebase options ----------------------------- byte l1 = (14-tbpos[1][tb])/2; // set pointer for timebase label length byte l2 = tbpos[0][tb]; // set pointer for timebase label start position byte i = 0; if(tb==8) { l1=9; } // Adjust postion for 1s as no mS label required Wire.beginTransmission(addr); Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0xB4); // Command: Set page addr B4H = Page 4 Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x15); // Command: Set col addr high bits 10H-1FH => 0n-Fn Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x02); // Command: Set col addr low bits 0H-0FH => 0n-Fn Wire.write(0x40); // Control: Data bytes data to follow while(i>1); l2++; i++; } for(l2=33;l2<=42;l2++) { if(tb==8) { Wire.write(0); } else { Wire.write(tbval[l2]); } } while(i<13) { Wire.write(0); i++; } Wire.endTransmission(); } void trigmenu() { // ----------------------- Menu display trigger options ----------------------------- Wire.beginTransmission(addr); Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0xB6); // Command: Set page addr B6H = Page 6 Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x15); // Command: Set col addr high bits 10H-1FH => 0n-Fn Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x07); // Command: Set col addr low bits 0H-0FH => 0n-Fn Wire.write(0x40); // Control: Data bytes data to follow for(byte i=0;i<=13;i++) { Wire.write(triglabels[i+trigmode*14]); } Wire.endTransmission(); } void oscsweep() { // -------------------------- ADC data capture ------------------------------------ int d2,tbc; bool inc = 0; byte bartop,barbot,vnext; byte vcur; // Value of Current sample byte triglevel = trigfall^trigval; // Invert Triglevel for falling trigger int d1 = 0; // Set initial sample different to 0 for manual and freerun trigger mode trigger = trigmode; // Auto = 0/1, Man = 2, Free = 3 do { bitSet(ADCSRA, ADSC); // Start ADC conversion delayMicroseconds(17); // Wait for conversion vcur = ADCH; // Set current ADC sample if(trigmode<=1) { // auto modes only if((vcur^trigfall)triglevel) { trigger = 4; } if(RE1int) { timebaseset(); trigger = 3; } if(RE2int) { triggerset(); trigger = 3; } } if(!digitalRead(Menubutton)) { menuint = true; trigger = 3; } if(!digitalRead(RE2sw)) { trigger=3; } } while(trigger<3); // Wait for trigger if(menuint) { return; } bitSet(ADCSRA, ADSC); // Start ADC conversion cyctim = micros()+modecycle[tb]; if(trigger==4) { d1=2*(vcur-trigval); } // auto trigger only otherwise d1 = 0 while(micros()0) { bitSet(ADCSRA, ADSC); // Start ADC conversion d2 = vcur-vnext; // Difference between ADC samples 1 and 2 bartop = vcur-(min(d1,d2)-1)/2; // Find position of the top of the bar bartop = (1+max(vcur,bartop))/4; Aval[cas][0]=max(Aval[cas][0],bartop); barbot = vcur-(max(d1,d2)+1)/2; // Find position of the bottom of the bar barbot = (1+min(vcur,barbot))/4; Aval[cas][1]=min(Aval[cas][1],barbot); d1 = -d2; // Change polarity of d2 to give d1 for the next ADC sample tbc--; vcur = vnext; inc = !inc; cyctim = cyctim+modecycle[tb]+inc; while(micros()> barbot%8; // Split the bar bottom into bit pattern barbot = 7-barbot/8; // and the postion of the bottom page if(bartop==barbot) { // If top and bottom are on the same page topbits &= botbits; // combine the top and bottom bit patterns botbits = 0; // and remove residual bottom bit pattern } while(j 0n-Fn Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x02); // Command: Set col addr low bits 0H-0FH => 0n-Fn Wire.write(0x40); // Control: Data bytes data to follow for(i=0; i<=15; i++) { Wire.write(Block[i][j]); // Write 16 columns on each of the 8 pages } Wire.endTransmission(); } } } void timebaselabel() { // ---------------------- Timebase label ---------------------------- byte l1 = 15-tbpos[1][tb]; // set pointer for timebase label length byte l2 = tbpos[0][tb]; // set pointer for timebase label start position Wire.beginTransmission(addr); // Set up SH1106 display Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0xB7); // Command: Command: Set page addr B7H = Page 7 Wire.write(0x80); // Control: 1 Command data to follow Wire.write(0x17); // Command: Set col addr high bits 10H-1FH => 0n-Fn Wire.write(0x80); // Control: 1 Command data to follow Wire.write(l1); // Command: Set col addr low bits 0H-0FH => 0n-Fn Wire.write(0x40); // Control: Data bytes data to follow Wire.write(0); Wire.write(0); while(l1<15) { Wire.write(tbval[l2]); l1++; l2++; } Wire.write(0); Wire.endTransmission(); } void Menuoptset() { // ---------------------- Menu option set ---------------------------- byte newmo = Menuopt; // Temporary store for new Menu option if(RE2dirn) { if(newmo>1) { newmo--; } } // decrement for anti-clockwise turn else { if(newmo<3) { newmo++; } } // increment for clockwise turn if(Menuopt != newmo) { // Check if Menu option has changed dispactive(0); // if so, clear the original active mode markers, Menuopt = newmo; // update Menu option dispactive(0xF0); // and add the new active mode markers } while(!digitalRead(RE2chA)) { } // Wait till Rotary Encoder ch A goes low again RE2int = false; // then reset interrupt flag } void timebaseset() { // ---------------------- Timebase set ---------------------------- if(!tb) { // Process timebase mode 0 (fastest) first to minimise processing delay if(!RE1dirn) { tb++; // Increment timebase mode for clockwise turn, otherwise ignore bitSet(optlimits,2); // No longer at the lower limit } } else { // Process other timebase modes if(RE1dirn) { tb--; // decrement timebase mode for anti-clockwise turn if(tb==7) { bitSet(optlimits,6); } // No longer at the upper limit if(!tb) { bitClear(optlimits,2); } // At the lower limit } else { tb++; } // increment timebase mode for clockwise turn if(tb>=8) { tb=8; // Don't let the timebase mode go beyond 8 bitClear(optlimits,6); // At the upper limit } } while(!digitalRead(RE1chA)) { } // Wait till Rotary Encoder ch A goes low again RE1int = false; // then reset interrupt flag } void trigmodeset() { // ---------------------- Trigger mode set ------------------------- int newtm = trigmode; if(!RE1dirn) { newtm++; } else { newtm--; } if(newtm<0 || newtm>3) {newtm = trigmode; } if(newtm!=trigmode) { if(newtm==3) { bitClear(optlimits,7); } else { bitSet(optlimits,7); } if(!newtm) { bitClear(optlimits,3); } else { bitSet(optlimits,3); } } trigmode = newtm; if(trigmode==1) { trigfall=0xFF; } else { trigfall=0; } while(!digitalRead(RE1chA)) { } // Wait till Rotary Encoder ch A goes low again RE1int = false; // then reset interrupt flag } void triggerset() { // ---------------------- Trigger set ---------------------------- if(!RE2dirn) { if(trigval>7) { trigval=trigval-4; } } // decrement trigger for a clockwise turn else { trigval=trigval+4; } // increment trigger for an anti-clockwise turn if(trigval>244) { trigval=244; } // Don't let trigger go above 253 trigblk = 7-(trigval>>5); // set page for trigger block byte trigvx = trigval>>2; // set 0-61 scale version of trigger value trigbit = 0xE0>>(trigvx%8); // set bits for trigger block trigb2 = 0xC0<<(7-trigvx%8); // set trigger block bits on next page if any if(trigb2) { // If trigger block overflows, trigblk--; // move it up a page trigbit = trigb2; // and swap bit patterns trigb2 = 0xE0>>(trigvx%8); } while(!digitalRead(RE2chA)) { } // Wait till Rotary Encoder ch A goes low again RE2int = false; // then reset interrupt flag } void RE1move() { // ---------------------- Timebase interrupt ---------------------------- if(!RE1int) { // Check previous interrupt has been processed RE1dirn=digitalRead(RE1chB); // Save direction of movement (0 = clockwise) RE1int = true; // Reset interrupt flag } } void RE2move() { // ---------------------- Trigger interrupt ---------------------------- if(!RE2int) { // Check previous interrupt has been processed RE2dirn=digitalRead(RE2chB); // Save direction of movement (0 = clockwise) RE2int = true; // Reset interrupt flag } }