├── M5StickC.jpg ├── README.md └── M5StickCOscilloscope.ino /M5StickC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z4ziggy/M5StickCOscilloscope/HEAD/M5StickC.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # M5StickC ESP32 Oscilloscope 2 | 3 | A fully functional oscilloscope based on ESp32 M5StickC 4 | ported from https://github.com/botofancalin/M5Stack-ESP32-Oscilloscope 5 | 6 | ![image](M5StickC.jpg) 7 | 8 | ## Changes from original 9 | - Channel 1 Input: Pin 26 10 | - Channel 2 input: Pin 36 11 | - PWM signal Output: Pin 33 12 | - SignaDelta Output: Pin 32 13 | - Key B: Menu selection 14 | - Key A: Next Selected item (loop) 15 | 16 | -------------------------------------------------------------------------------- /M5StickCOscilloscope.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | const int LCD_WIDTH = 160; 4 | const int LCD_HEIGHT = 80; 5 | const int SAMPLES = 160; 6 | const int DOTS_DIV = 10; 7 | 8 | const int ad_ch0 = 26; // Analog 26 pin for channel 0 9 | const int ad_ch1 = 36; // Analog 36 pin for channel 1 10 | const long VREF[] = { 250, 500, 1250, 2500, 5000 }; 11 | const int MILLIVOL_per_dot[] = { 33, 17, 6, 3, 2 }; 12 | const int MODE_ON = 0; 13 | const int MODE_INV = 1; 14 | const int MODE_OFF = 2; 15 | const char *Modes[] = { "NORM", "INV", "OFF" }; 16 | const int TRIG_AUTO = 0; 17 | const int TRIG_NORM = 1; 18 | const int TRIG_SCAN = 2; 19 | const char *TRIG_Modes[] = { "Auto", "Norm", "Scan" }; 20 | const int TRIG_E_UP = 0; 21 | const int TRIG_E_DN = 1; 22 | #define RATE_MIN 0 23 | #define RATE_MAX 13 24 | const char *Rates[] = { "F1-1", "F1-2", " F2", " 5ms", "10ms", "20ms", "50ms", "0.1s", "0.2s", "0.5s", "1s", "2s", "5s", "10s" }; 25 | short rate = 3; 26 | #define RANGE_MIN 0 27 | #define RANGE_MAX 4 28 | const char *Ranges[] = { " 1V", "0.5V", "0.2V", "0.1V", "50mV" }; 29 | int range0 = RANGE_MIN; 30 | short range1 = RANGE_MIN; 31 | short ch0_mode = MODE_ON; 32 | short ch1_mode = MODE_ON; 33 | int ch0_off = 0; 34 | int ch1_off = 0; 35 | short trig_mode = TRIG_AUTO; 36 | int trig_lv = 40; 37 | short trig_edge = TRIG_E_UP; 38 | short trig_ch = 0; 39 | short Start = 1; 40 | short menu = 19; 41 | short data[4][SAMPLES]; // keep twice of the number of channels to make it a double buffer 42 | short sample = 0; // index for double buffer 43 | int amplitude = 0; 44 | int amplitudeStep = 5; 45 | 46 | TaskHandle_t LedC_Gen; 47 | TaskHandle_t SigmaDeltaGen; 48 | 49 | /////////////////////////////////////////////////////////////////////////////////////////////// 50 | #define CH1COLOR YELLOW 51 | #define CH2COLOR CYAN 52 | #define GREY 0x7BEF 53 | 54 | int lastmenu = 0; 55 | void DrawText() 56 | { 57 | // if (lastmenu == menu) 58 | // return; 59 | //` lastmenu = menu; 60 | M5.Lcd.setRotation(0); 61 | if (menu > 19) 62 | { 63 | M5.Lcd.fillRect(5, menu - 10, 40, 10, BLACK); 64 | } 65 | else if (menu == 19) 66 | { 67 | M5.Lcd.fillRect(5, 129, 40, 10, BLACK); 68 | } 69 | //DrawGrid(); 70 | 71 | M5.Lcd.fillRect(5, menu, 40, 10, BLUE); 72 | 73 | M5.Lcd.drawString((Start == 1 ? "Stop" : "Run"), 5, 20); 74 | M5.Lcd.drawString(String(String(Ranges[range0]) + "/DIV"), 5, 30); 75 | M5.Lcd.drawString(String(String(Ranges[range1]) + "/DIV"), 5, 40); 76 | M5.Lcd.drawString(String(String(Rates[rate]) + "/DIV"), 5, 50); 77 | M5.Lcd.drawString(String(Modes[ch0_mode]), 5, 60); 78 | M5.Lcd.drawString(String(Modes[ch1_mode]), 5, 70); 79 | 80 | M5.Lcd.drawString(String("OFS1:" + String(ch0_off)), 5, 80); 81 | M5.Lcd.drawString(String("OFS2:" + String(ch1_off)), 5, 90); 82 | M5.Lcd.drawString(String(trig_ch == 0 ? "T:1" : "T:2"), 5, 100); 83 | M5.Lcd.drawString(String(TRIG_Modes[trig_mode]), 5, 110); 84 | M5.Lcd.drawString(String("Tlv:" + String(trig_lv)), 5, 120); 85 | M5.Lcd.drawString(String((trig_edge == TRIG_E_UP) ? "T:UP" : "T:DN"), 5, 130); 86 | 87 | M5.Lcd.setRotation(3); 88 | } 89 | 90 | void CheckSW() 91 | { 92 | M5.update(); 93 | if (M5.BtnA.wasPressed()) 94 | { 95 | (menu < 129) ? (menu += 10) : (menu = 19); 96 | DrawText(); 97 | return; 98 | } 99 | 100 | if (M5.BtnB.wasPressed()) 101 | { 102 | switch (menu) 103 | { 104 | case 19: 105 | Start = !Start; 106 | break; 107 | case 29: 108 | if (++range0 > RANGE_MAX) 109 | range0 = RANGE_MIN; 110 | break; 111 | case 39: 112 | if (++range1 > RANGE_MAX) 113 | range1 = RANGE_MIN; 114 | break; 115 | case 49: 116 | if (++rate > RATE_MAX) 117 | rate = RATE_MIN; 118 | break; 119 | case 59: 120 | if (++ch0_mode > MODE_OFF) 121 | ch0_mode = MODE_ON; 122 | break; 123 | case 69: 124 | if (++ch1_mode > MODE_OFF) 125 | ch1_mode = MODE_ON; 126 | break; 127 | case 79: 128 | if (ch0_off < 4095) 129 | ch0_off += 4096 / VREF[range0]; 130 | else 131 | ch0_off = 0; 132 | break; 133 | case 89: 134 | if (ch1_off < 4095) 135 | ch1_off += 4096 / VREF[range1]; 136 | else 137 | ch1_off = 0; 138 | break; 139 | case 99: 140 | trig_ch = !trig_ch; 141 | break; 142 | case 109: 143 | if (++trig_mode > TRIG_SCAN) 144 | trig_mode = TRIG_AUTO; 145 | break; 146 | case 119: 147 | if (++trig_lv > 60) 148 | trig_lv = 0; 149 | break; 150 | case 129: 151 | trig_edge = !trig_edge; 152 | break; 153 | } 154 | DrawText(); 155 | return; 156 | } 157 | } 158 | 159 | void DrawGrid() 160 | { 161 | for (int x = 0; x <= SAMPLES; x += 2) // Horizontal Line 162 | { 163 | for (int y = 0; y <= LCD_HEIGHT; y += DOTS_DIV) 164 | { 165 | M5.Lcd.drawPixel(x, y, GREY); 166 | CheckSW(); 167 | } 168 | if (LCD_HEIGHT == 80) 169 | { 170 | M5.Lcd.drawPixel(x, LCD_HEIGHT - 1, GREY); 171 | } 172 | } 173 | for (int x = 0; x <= SAMPLES; x += DOTS_DIV) // Vertical Line 174 | { 175 | for (int y = 0; y <= LCD_HEIGHT; y += 2) 176 | { 177 | M5.Lcd.drawPixel(x, y, GREY); 178 | CheckSW(); 179 | } 180 | } 181 | } 182 | 183 | void DrawGrid(int x) 184 | { 185 | if ((x % 2) == 0) 186 | { 187 | for (int y = 0; y <= LCD_HEIGHT; y += DOTS_DIV) 188 | { 189 | M5.Lcd.drawPixel(x, y, GREY); 190 | } 191 | } 192 | if ((x % DOTS_DIV) == 0) 193 | { 194 | for (int y = 0; y <= LCD_HEIGHT; y += 2) 195 | { 196 | M5.Lcd.drawPixel(x, y, GREY); 197 | } 198 | } 199 | } 200 | 201 | void ClearAndDrawGraph() 202 | { 203 | int clear = 0; 204 | 205 | if (sample == 0) 206 | { 207 | clear = 2; 208 | } 209 | for (int x = 0; x < (SAMPLES - 1); x++) 210 | { 211 | M5.Lcd.drawLine(x, LCD_HEIGHT - data[clear + 0][x], x + 1, LCD_HEIGHT - data[clear + 0][x + 1], BLACK); 212 | M5.Lcd.drawLine(x, LCD_HEIGHT - data[clear + 1][x], x + 1, LCD_HEIGHT - data[clear + 1][x + 1], BLACK); 213 | if (ch0_mode != MODE_OFF) 214 | { 215 | M5.Lcd.drawLine(x, LCD_HEIGHT - data[sample + 0][x], x + 1, LCD_HEIGHT - data[sample + 0][x + 1], CH1COLOR); 216 | } 217 | if (ch1_mode != MODE_OFF) 218 | { 219 | M5.Lcd.drawLine(x, LCD_HEIGHT - data[sample + 1][x], x + 1, LCD_HEIGHT - data[sample + 1][x + 1], CH2COLOR); 220 | } 221 | CheckSW(); 222 | } 223 | } 224 | 225 | void ClearAndDrawDot(int i) 226 | { 227 | int clear = 0; 228 | 229 | if (i <= 1) 230 | { 231 | return; 232 | } 233 | if (sample == 0) 234 | { 235 | clear = 2; 236 | } 237 | M5.Lcd.drawLine(i - 1, LCD_HEIGHT - data[clear + 0][i - 1], i, LCD_HEIGHT - data[clear + 0][i], BLACK); 238 | M5.Lcd.drawLine(i - 1, LCD_HEIGHT - data[clear + 1][i - 1], i, LCD_HEIGHT - data[clear + 1][i], BLACK); 239 | if (ch0_mode != MODE_OFF) 240 | { 241 | M5.Lcd.drawLine(i - 1, LCD_HEIGHT - data[sample + 0][i - 1], i, LCD_HEIGHT - data[sample + 0][i], CH1COLOR); 242 | } 243 | if (ch1_mode != MODE_OFF) 244 | { 245 | M5.Lcd.drawLine(i - 1, LCD_HEIGHT - data[sample + 1][i - 1], i, LCD_HEIGHT - data[sample + 1][i], CH2COLOR); 246 | } 247 | DrawGrid(i); 248 | } 249 | 250 | inline long adRead(short ch, short mode, int off) 251 | { 252 | long a = analogRead(ch); 253 | a = (((a + off) * VREF[(ch == ad_ch0) ? range0 : range1]) / 10000UL) + 30; 254 | a = ((a >= LCD_HEIGHT) ? LCD_HEIGHT : a); 255 | if (mode == MODE_INV) 256 | { 257 | return LCD_HEIGHT - a; 258 | } 259 | return a; 260 | } 261 | 262 | void ledcAnalogWrite(uint8_t channel, uint32_t value, uint32_t valueMax = 255) 263 | { 264 | uint32_t duty = (8191 / valueMax) * min(value, valueMax); 265 | ledcWrite(channel, duty); 266 | } 267 | 268 | // Make a PWM generator task on core 0 269 | // Signal generator pin 2 270 | void LedC_Task(void *parameter) 271 | { 272 | ledcSetup(0, 50, 13); 273 | ledcAttachPin(33, 0); 274 | 275 | for (;;) 276 | { 277 | while(!Start) { ledcWrite(0,-1); delay(1); }; 278 | ledcAnalogWrite(0, amplitude); 279 | amplitude = amplitude + amplitudeStep; 280 | if (amplitude <= 0 || amplitude >= 255) 281 | { 282 | amplitudeStep = -amplitudeStep; 283 | } 284 | delay(30); 285 | } 286 | vTaskDelete(NULL); 287 | } 288 | 289 | void SigmaDelta_Task(void *parameter) 290 | { 291 | sigmaDeltaSetup(0, 312500); 292 | sigmaDeltaAttachPin(32, 0); 293 | sigmaDeltaWrite(0, 0); 294 | for (;;) 295 | { 296 | static uint8_t i = 0; 297 | sigmaDeltaWrite(0, i++); 298 | delayMicroseconds(50); 299 | } 300 | vTaskDelete(NULL); 301 | } 302 | void setup() 303 | { 304 | M5.begin(); 305 | M5.Lcd.setRotation(3); 306 | M5.Lcd.setTextSize(1); 307 | M5.Lcd.setTextColor(WHITE); 308 | 309 | M5.Lcd.fillScreen(BLACK); 310 | DrawGrid(); 311 | DrawText(); 312 | //M5.Lcd.setBrightness(100); 313 | dacWrite(25, 0); 314 | 315 | xTaskCreatePinnedToCore( 316 | LedC_Task, /* Task function. */ 317 | "LedC_Task", /* name of the task, a name just for humans */ 318 | 8192, /* Stack size of task */ 319 | NULL, /* parameter of the task */ 320 | 1, /* priority of the task */ 321 | &LedC_Gen, /* Task handle to keep track of the created task */ 322 | 1); /*cpu core number where the task is assigned*/ 323 | 324 | xTaskCreatePinnedToCore( 325 | SigmaDelta_Task, /* Task function. */ 326 | "SigmaDelta_Task", /* name of task, a name just for humans */ 327 | 8192, /* Stack size of task */ 328 | NULL, /* parameter of the task */ 329 | 1, /* priority of the task */ 330 | &SigmaDeltaGen, /* Task handle to keep track of the created task */ 331 | 1); /*cpu core number where the task is assigned*/ 332 | 333 | } 334 | 335 | void loop() 336 | { 337 | if (trig_mode != TRIG_SCAN) 338 | { 339 | unsigned long st = millis(); 340 | short oad = (trig_ch == 0) ? (adRead(ad_ch0, ch0_mode, ch0_off)) : (adRead(ad_ch1, ch1_mode, ch1_off)); 341 | for (;;) 342 | { 343 | short ad; 344 | if (trig_ch == 0) 345 | { 346 | ad = adRead(ad_ch0, ch0_mode, ch0_off); 347 | } 348 | else 349 | { 350 | ad = adRead(ad_ch1, ch1_mode, ch1_off); 351 | } 352 | 353 | if (trig_edge == TRIG_E_UP) 354 | { 355 | if (ad >= trig_lv && ad > oad) 356 | { 357 | break; 358 | } 359 | } 360 | else 361 | { 362 | if (ad <= trig_lv && ad < oad) 363 | { 364 | break; 365 | } 366 | } 367 | oad = ad; 368 | 369 | CheckSW(); 370 | if (trig_mode == TRIG_SCAN) 371 | { 372 | break; 373 | } 374 | if (trig_mode == TRIG_AUTO && (millis() - st) > 100) 375 | { 376 | break; 377 | } 378 | } 379 | } 380 | 381 | // sample and draw depending on the sampling rate 382 | if (rate <= 5 && Start) 383 | { 384 | (sample == 0) ? (sample = 2) : (sample = 0); // change the index for the double buffer 385 | 386 | if (rate == 0) // full speed, channel 0 only 387 | { 388 | for (int i = 0; i < SAMPLES; i++) 389 | { 390 | data[sample + 0][i] = adRead(ad_ch0, ch0_mode, ch0_off); 391 | } 392 | for (int i = 0; i < SAMPLES; i++) 393 | { 394 | data[sample + 1][i] = 0; 395 | } 396 | } 397 | else if (rate == 1) // full speed, channel 1 only 398 | { 399 | for (int i = 0; i < SAMPLES; i++) 400 | { 401 | data[sample + 1][i] = adRead(ad_ch1, ch1_mode, ch1_off); 402 | } 403 | for (int i = 0; i < SAMPLES; i++) 404 | { 405 | data[sample + 0][i] = 0; 406 | } 407 | } 408 | else if (rate == 2) // full speed, dual channel 409 | { 410 | for (int i = 0; i < SAMPLES; i++) 411 | { 412 | data[sample + 0][i] = adRead(ad_ch0, ch0_mode, ch0_off); 413 | data[sample + 1][i] = adRead(ad_ch1, ch1_mode, ch1_off); 414 | } 415 | } 416 | else if (rate >= 3 && rate <= 5) // .5ms, 1ms or 2ms sampling 417 | { 418 | const unsigned long r_[] = { 5000 / DOTS_DIV, 10000 / DOTS_DIV, 20000 / DOTS_DIV }; 419 | unsigned long st = micros(); 420 | unsigned long r = r_[rate - 3]; 421 | for (int i = 0; i < SAMPLES; i++) 422 | { 423 | while ((st - micros()) < r) 424 | { 425 | ; 426 | } 427 | st += r; 428 | data[sample + 0][i] = adRead(ad_ch0, ch0_mode, ch0_off); 429 | data[sample + 1][i] = adRead(ad_ch1, ch1_mode, ch1_off); 430 | } 431 | } 432 | ClearAndDrawGraph(); 433 | CheckSW(); 434 | DrawGrid(); 435 | DrawText(); 436 | } 437 | else if (Start) 438 | { // 5ms - 500ms sampling 439 | // copy currently showing data to another 440 | if (sample == 0) 441 | { 442 | for (int i = 0; i < SAMPLES; i++) 443 | { 444 | data[2][i] = data[0][i]; 445 | data[3][i] = data[1][i]; 446 | } 447 | } 448 | else 449 | { 450 | for (int i = 0; i < SAMPLES; i++) 451 | { 452 | data[0][i] = data[2][i]; 453 | data[1][i] = data[3][i]; 454 | } 455 | } 456 | 457 | const unsigned long r_[] = { 50000 / DOTS_DIV, 100000 / DOTS_DIV, 200000 / DOTS_DIV, 458 | 500000 / DOTS_DIV, 1000000 / DOTS_DIV, 2000000 / DOTS_DIV, 459 | 5000000 / DOTS_DIV, 10000000 / DOTS_DIV }; 460 | unsigned long st = micros(); 461 | for (int i = 0; i < SAMPLES; i++) 462 | { 463 | while ((st - micros()) < r_[rate - 6]) 464 | { 465 | CheckSW(); 466 | if (rate < 6) 467 | { 468 | break; 469 | } 470 | } 471 | if (rate < 6) // sampling rate has been changed 472 | { 473 | break; 474 | } 475 | st += r_[rate - 6]; 476 | if (st - micros() > r_[rate - 6]) // sampling rate has been changed to shorter interval 477 | { 478 | st = micros(); 479 | } 480 | if (!Start) 481 | { 482 | i--; 483 | continue; 484 | } 485 | data[sample + 0][i] = adRead(ad_ch0, ch0_mode, ch0_off); 486 | data[sample + 1][i] = adRead(ad_ch1, ch1_mode, ch1_off); 487 | ClearAndDrawDot(i); 488 | } 489 | DrawGrid(); 490 | DrawText(); 491 | } 492 | else 493 | { 494 | CheckSW(); 495 | } 496 | M5.update(); 497 | } 498 | --------------------------------------------------------------------------------