├── README.md ├── STM32_Oscope_Spectrum_SD.ino └── STM32_oscilloscope_schematic.png /README.md: -------------------------------------------------------------------------------- 1 | # STM32-Oscilloscope 2 | STM32 Digital Oscilloscope using the STM32F103C8 MCU and the NT35702 2.4 inch TFT display. 3 | 4 | Specifications: 5 | 6 | * One input channel 7 | * 2.57 Msps ADC, accepting signal frequencies up to 1.28 MHz 8 | * Calculates minimum, maximum and average values 9 | * Spectrum FFT analysis 10 | * Fundamental frequency detection 11 | * SD card export of signal wave shape and spectrum 12 | * Freeze function 13 | * Sampling rate selection 14 | 15 | Further details on: https://www.gameinstance.com/post/80/STM32-Oscilloscope-with-FFT-and-SD-export 16 | -------------------------------------------------------------------------------- /STM32_Oscope_Spectrum_SD.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * STM32 Digital Oscilloscope 3 | * using the STM32F103C8 MCU and the NT35702 2.4 inch TFT display 4 | * https://www.gameinstance.com/post/80/STM32-Oscilloscope-with-FFT-and-SD-export 5 | * 6 | * GameInstance.com 7 | * 2016-2018 8 | */ 9 | #include 10 | #include 11 | #include 12 | #include "SdFat.h" 13 | 14 | #include 15 | #include 16 | 17 | static const uint8_t SD_CHIP_SELECT = PB12; 18 | static const uint8_t TIME_BUTTON = PA15; 19 | static const uint8_t TRIGGER_BUTTON = PB10; 20 | static const uint8_t FREEZE_BUTTON = PB11; 21 | static const uint8_t TEST_SIGNAL = PA8; 22 | static const uint8_t CHANNEL_1 = PB0; 23 | static const uint8_t CHANNEL_2 = PB1; 24 | 25 | static const uint16_t BLACK = 0x0000; 26 | static const uint16_t BLUE = 0x001F; 27 | static const uint16_t RED = 0xF800; 28 | static const uint16_t GREEN = 0x07E0; 29 | static const uint16_t CYAN = 0x07FF; 30 | static const uint16_t MAGENTA = 0xF81F; 31 | static const uint16_t YELLOW = 0xFFE0; 32 | static const uint16_t WHITE = 0xFFFF; 33 | 34 | static const uint16_t BACKGROUND_COLOR = BLUE; 35 | static const uint16_t DIV_LINE_COLOR = GREEN; 36 | static const uint16_t CH1_SIGNAL_COLOR = YELLOW; 37 | 38 | static const uint16_t ADC_RESOLUTION = 4096; // units 39 | static const uint16_t EFFECTIVE_VERTICAL_RESOLUTION = 200; // pixels 40 | static const uint16_t SCREEN_HORIZONTAL_RESOLUTION = 320; // pixels 41 | static const uint16_t SCREEN_VERTICAL_RESOLUTION = 240; // pixels 42 | static const uint16_t DIVISION_SIZE = 40; // pixels 43 | static const uint16_t SUBDIVISION_SIZE = 8; // pixels (DIVISION_SIZE / 5) 44 | static const uint16_t BUFFER_SIZE = 1024; // bytes 45 | static const uint8_t TRIGGER_THRESOLD = 127; // units 46 | static const float ADC_SCREEN_FACTOR = (float)EFFECTIVE_VERTICAL_RESOLUTION / (float)ADC_RESOLUTION; 47 | static const float VCC_3_3 = 3.3; // volts 48 | 49 | const uint8_t DT_DT[] = {4, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1}; 50 | const uint8_t DT_PRE[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; 51 | const uint8_t DT_SMPR[] = {0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 7}; 52 | const float DT_FS[] = {2571, 2571, 2571, 1800, 1384, 878, 667, 529, 429, 143, 71.4}; 53 | const float DT_DIV[] = {3.9, 7.81, 15.63, 22.73, 29.41, 45.45, 55.55, 83.33, 95.24, 293.3, 586.6}; 54 | 55 | Adafruit_ILI9341_8bit_STM tft; 56 | SdFat sd(2); 57 | SdFile file; 58 | 59 | uint8_t bk[SCREEN_HORIZONTAL_RESOLUTION]; 60 | uint16_t data16[BUFFER_SIZE]; 61 | uint32_t data32[BUFFER_SIZE]; 62 | uint32_t y[BUFFER_SIZE]; 63 | uint8_t time_base = 7; 64 | uint16_t i, j; 65 | uint8_t state = 0; 66 | uint16_t maxy, avgy, miny; 67 | 68 | volatile uint8_t h = 1, h2 = -1; 69 | volatile uint8_t trigger = 1, freeze = 0; 70 | volatile bool bPress[3], bTitleChange = true, bScreenChange = true; 71 | volatile static bool dma1_ch1_Active; 72 | 73 | bool wasPressed(int pin, int index) { 74 | // 75 | if (HIGH == digitalRead(pin)) { 76 | // isn't pressed 77 | if (bPress[index]) { 78 | // but was before 79 | bPress[index] = false; 80 | } 81 | return false; 82 | } 83 | // is pressed 84 | if (!bPress[index]) { 85 | // and wasn't before 86 | bPress[index] = true; 87 | return true; 88 | } 89 | // but was before 90 | return false; 91 | } 92 | 93 | // ------------------------------------------------------------------------------------ 94 | // The following section was inspired by http://www.stm32duino.com/viewtopic.php?t=1145 95 | 96 | void setADCs() { 97 | // 98 | switch (DT_PRE[time_base]) { 99 | // 100 | case 0: rcc_set_prescaler(RCC_PRESCALER_ADC, RCC_ADCPRE_PCLK_DIV_2); break; 101 | case 1: rcc_set_prescaler(RCC_PRESCALER_ADC, RCC_ADCPRE_PCLK_DIV_4); break; 102 | case 2: rcc_set_prescaler(RCC_PRESCALER_ADC, RCC_ADCPRE_PCLK_DIV_6); break; 103 | case 3: rcc_set_prescaler(RCC_PRESCALER_ADC, RCC_ADCPRE_PCLK_DIV_8); break; 104 | default: rcc_set_prescaler(RCC_PRESCALER_ADC, RCC_ADCPRE_PCLK_DIV_8); 105 | } 106 | switch (DT_SMPR[time_base]) { 107 | // 108 | case 0: adc_set_sample_rate(ADC1, ADC_SMPR_1_5); break; 109 | case 1: adc_set_sample_rate(ADC1, ADC_SMPR_7_5); break; 110 | case 2: adc_set_sample_rate(ADC1, ADC_SMPR_13_5); break; 111 | case 3: adc_set_sample_rate(ADC1, ADC_SMPR_28_5); break; 112 | case 4: adc_set_sample_rate(ADC1, ADC_SMPR_41_5); break; 113 | case 5: adc_set_sample_rate(ADC1, ADC_SMPR_55_5); break; 114 | case 6: adc_set_sample_rate(ADC1, ADC_SMPR_71_5); break; 115 | case 7: adc_set_sample_rate(ADC1, ADC_SMPR_239_5); break; 116 | default: adc_set_sample_rate(ADC1, ADC_SMPR_239_5); 117 | } 118 | adc_set_reg_seqlen(ADC1, 1); 119 | ADC1->regs->SQR3 = PIN_MAP[CHANNEL_1].adc_channel; 120 | ADC1->regs->CR2 |= ADC_CR2_CONT; // | ADC_CR2_DMA; // Set continuous mode and DMA 121 | ADC1->regs->CR2 |= ADC_CR2_SWSTART; 122 | } 123 | 124 | void real_to_complex(uint16_t * in, uint32_t * out, int len) { 125 | // 126 | for (int i = 0; i < len; i++) out[i] = in[i];// * 8; 127 | } 128 | 129 | uint16_t asqrt(uint32_t x) { //good enough precision, 10x faster than regular sqrt 130 | // 131 | int32_t op, res, one; 132 | op = x; 133 | res = 0; 134 | one = 1 << 30; 135 | while (one > op) one >>= 2; 136 | while (one != 0) { 137 | if (op >= res + one) { 138 | op = op - (res + one); 139 | res = res + 2 * one; 140 | } 141 | res /= 2; 142 | one /= 4; 143 | } 144 | return (uint16_t) (res); 145 | } 146 | 147 | void inplace_magnitude(uint32_t * target, uint16_t len) { 148 | // 149 | uint16_t * p16; 150 | for (int i = 0; i < len; i ++) { 151 | // 152 | int16_t real = target[i] & 0xFFFF; 153 | int16_t imag = target[i] >> 16; 154 | // target[i] = 10 * log10(real*real + imag*imag); 155 | uint32_t magnitude = asqrt(real*real + imag*imag); 156 | target[i] = magnitude; 157 | } 158 | } 159 | 160 | uint32_t perform_fft(uint32_t * indata, uint32_t * outdata, const int len) { 161 | // 162 | cr4_fft_1024_stm32(outdata, indata, len); 163 | inplace_magnitude(outdata, len); 164 | } 165 | 166 | static void DMA1_CH1_Event() { 167 | // 168 | dma1_ch1_Active = 0; 169 | } 170 | 171 | void adc_dma_enable(const adc_dev * dev) { 172 | // 173 | bb_peri_set_bit(&dev->regs->CR2, ADC_CR2_DMA_BIT, 1); 174 | } 175 | // ------------------------------------------------------------------------------------ 176 | 177 | void export_to_sd() { 178 | // 179 | tft.setCursor(170, 20); 180 | tft.setTextColor(WHITE); 181 | tft.setTextSize(1); 182 | tft.print("Writing to SD ..."); 183 | tft.setCursor(170, 20); 184 | if (!sd.cardBegin(SD_CHIP_SELECT, SD_SCK_HZ(F_CPU/4))) { 185 | // 186 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 187 | tft.print("No SD card detected"); 188 | return; 189 | } 190 | delay(500); 191 | if (!sd.fsBegin()) { 192 | // 193 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 194 | tft.print("File system init failed."); 195 | return; 196 | } 197 | uint8_t index; 198 | if (!sd.exists("DSO")) { 199 | // no pre-exising folder structure 200 | if (!sd.mkdir("DSO")) { 201 | // 202 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 203 | tft.print("Can't create folder"); 204 | return; 205 | } 206 | } 207 | if (!sd.exists("DSO/data.idx")) { 208 | // no index file 209 | index = 1; 210 | if (!file.open("DSO/data.idx", O_CREAT | O_WRITE)) { 211 | // 212 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 213 | tft.print("Can't create idx file"); 214 | return; 215 | } 216 | file.write(index); 217 | if (!file.sync() || file.getWriteError()) { 218 | // 219 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 220 | tft.print("Idx file write error"); 221 | return; 222 | } 223 | file.close(); 224 | } else { 225 | // 226 | if (!file.open("DSO/data.idx", O_READ)) { 227 | // 228 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 229 | tft.print("Can't open idx file"); 230 | return; 231 | } 232 | if (!file.read(&index, 1)) { 233 | // 234 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 235 | tft.print("Can't read idx file"); 236 | return; 237 | } 238 | if (!file.sync() || file.getWriteError()) { 239 | // 240 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 241 | tft.print("File write error"); 242 | return; 243 | } 244 | file.close(); 245 | } 246 | String s = "DSO/Exp"; 247 | s += index; 248 | s += ".dat"; 249 | if (!file.open(s.c_str(), O_CREAT | O_WRITE | O_EXCL)) { 250 | // 251 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 252 | tft.print("Can't data create file"); 253 | return; 254 | } 255 | file.println("Time series"); 256 | for (uint16_t i = 0; i < BUFFER_SIZE; i ++) { 257 | // 258 | file.print(data16[i], DEC);file.print(", "); 259 | } 260 | file.println(" "); 261 | file.print("Fs: ");file.print(DT_FS[time_base]);file.println("kHz"); 262 | file.println("Spectrum"); 263 | for (uint16_t i = 0; i < BUFFER_SIZE/2; i ++) { 264 | // 265 | file.print(y[i], DEC);file.print(", "); 266 | } 267 | file.println(" "); 268 | if (!file.sync() || file.getWriteError()) { 269 | // 270 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 271 | tft.print("File write error"); 272 | return; 273 | } 274 | file.close(); 275 | s += ".img"; 276 | 277 | if (!file.open(s.c_str(), O_CREAT | O_WRITE | O_EXCL)) { 278 | // 279 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 280 | tft.print("Can't image create file"); 281 | return; 282 | } 283 | file.println("IMX"); 284 | for (uint16_t i = 0; i < BUFFER_SIZE; i ++) { 285 | // 286 | file.print(data16[i], DEC);file.print(", "); 287 | } 288 | file.println(" "); 289 | file.print("Fs: ");file.print(DT_FS[time_base]);file.println("kHz"); 290 | file.println("Spectrum"); 291 | for (uint16_t i = 0; i < BUFFER_SIZE/2; i ++) { 292 | // 293 | file.print(y[i], DEC);file.print(", "); 294 | } 295 | file.println(" "); 296 | if (!file.sync() || file.getWriteError()) { 297 | // 298 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 299 | tft.print("File write error"); 300 | return; 301 | } 302 | file.close(); 303 | 304 | index ++; 305 | if (!file.open("DSO/data.idx", O_CREAT | O_WRITE)) { 306 | // 307 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 308 | tft.print("Can't create idx file"); 309 | return; 310 | } 311 | file.write(index); 312 | if (!file.sync() || file.getWriteError()) { 313 | // 314 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 315 | tft.print("Idx file write error"); 316 | return; 317 | } 318 | file.close(); 319 | tft.fillRect(169, 19, 150, 9, BACKGROUND_COLOR); 320 | tft.print("File write success"); 321 | delay(2000); 322 | tft.fillRect(170, 19, 150, 9, BACKGROUND_COLOR); 323 | } 324 | 325 | void setup() { 326 | // 327 | tft.begin(); 328 | tft.setRotation(3); 329 | 330 | bPress[0] = false; 331 | bPress[1] = false; 332 | bPress[2] = false; 333 | 334 | adc_calibrate(ADC1); 335 | } 336 | 337 | void loop() { 338 | // 339 | if (state == 0) { 340 | // 341 | tft.fillScreen(BACKGROUND_COLOR); 342 | tft.setCursor(15, 100); 343 | tft.setTextColor(YELLOW); 344 | tft.setTextSize(3); 345 | tft.println("GameInstance.com"); 346 | // analogWrite(TEST_SIGNAL, 127); 347 | delay(1500); 348 | tft.fillScreen(BACKGROUND_COLOR); 349 | state = 1; 350 | } 351 | if (state == 1) { 352 | // init 353 | state = 2; 354 | } 355 | if (state == 2) { 356 | // buttons check 357 | if (wasPressed(TIME_BUTTON, 0)) { 358 | // toggling the time division modes 359 | time_base ++; 360 | if (trigger == 0) { 361 | // spectrum 362 | if (time_base <= 2) time_base = 3; 363 | } 364 | time_base = time_base % sizeof(DT_DT); 365 | h = DT_DT[time_base]; 366 | bScreenChange = true; 367 | } 368 | if (wasPressed(TRIGGER_BUTTON, 1)) { 369 | // toggling the trigger mode 370 | trigger ++; 371 | trigger = trigger % 4; 372 | bScreenChange = true; 373 | bTitleChange = true; 374 | } 375 | if (wasPressed(FREEZE_BUTTON, 2)) { 376 | // toggling the freeze screen 377 | freeze = (freeze > 0) ? 0 : 3; 378 | bTitleChange = true; 379 | } 380 | if (freeze) { 381 | // frozen screen 382 | state = 5; 383 | } else { 384 | // live screen 385 | state = 3; 386 | } 387 | } 388 | if (state == 3) { 389 | // acquisition 390 | 391 | setADCs(); 392 | dma_init(DMA1); 393 | dma_attach_interrupt(DMA1, DMA_CH1, DMA1_CH1_Event); 394 | adc_dma_enable(ADC1); 395 | dma_setup_transfer(DMA1, DMA_CH1, &ADC1->regs->DR, DMA_SIZE_16BITS, data16, DMA_SIZE_16BITS, (DMA_MINC_MODE | DMA_TRNS_CMPLT)); 396 | dma_set_num_transfers(DMA1, DMA_CH1, BUFFER_SIZE); 397 | dma1_ch1_Active = 1; 398 | dma_enable(DMA1, DMA_CH1); // enable the DMA channel and start the transfer 399 | 400 | while (dma1_ch1_Active) {}; // waiting for the DMA to complete 401 | dma_disable(DMA1, DMA_CH1); // end of DMA trasfer 402 | 403 | real_to_complex(data16, data32, BUFFER_SIZE); // data format conversion 404 | perform_fft(data32, y, BUFFER_SIZE); // FFT computation 405 | 406 | state = 4; 407 | } 408 | if (state == 4) { 409 | // display signal screen 410 | if (bScreenChange) { 411 | // massive change on screen 412 | bScreenChange = false; 413 | tft.fillScreen(BACKGROUND_COLOR); 414 | bTitleChange = true; 415 | } else { 416 | // clear previous wave 417 | if (trigger == 0) { 418 | // clear previous spectrum 419 | for (i = 1; i < SCREEN_HORIZONTAL_RESOLUTION; i ++) { 420 | // 421 | tft.drawLine( 422 | i, 423 | bk[i], 424 | i + 1, 425 | bk[i + 1], 426 | BACKGROUND_COLOR); 427 | } 428 | } else { 429 | // clear previous time samples 430 | for (i = 0, j = 0; j < SCREEN_HORIZONTAL_RESOLUTION; i ++, j += h2) { 431 | // 432 | tft.drawLine( 433 | j, 434 | bk[i], 435 | j + h2, 436 | bk[i + 1], 437 | BACKGROUND_COLOR); 438 | } 439 | } 440 | 441 | } 442 | // re-draw the divisions 443 | for (i = 0; i < SCREEN_HORIZONTAL_RESOLUTION; i += DIVISION_SIZE) { 444 | // 445 | for (j = SCREEN_VERTICAL_RESOLUTION; j > 13; j -= ((i == 160) ? SUBDIVISION_SIZE : DIVISION_SIZE)) { 446 | // 447 | tft.drawLine(i - 1, j, i + 1, j, DIV_LINE_COLOR); 448 | } 449 | } 450 | for (i = SCREEN_VERTICAL_RESOLUTION; i > 13; i -= DIVISION_SIZE) { 451 | // 452 | for (j = 0; j < SCREEN_HORIZONTAL_RESOLUTION; j += ((i == 120) ? SUBDIVISION_SIZE : DIVISION_SIZE)) { 453 | // 454 | tft.drawLine(j, i - 1, j, i + 1, DIV_LINE_COLOR); 455 | } 456 | } 457 | // draw current wave 458 | if (trigger == 0) { 459 | // display spectrum 460 | uint16_t max_y = 0, max_x = 0; 461 | uint16_t i_0, i_1; 462 | bool hit_max = false; 463 | for (i = 1; i < BUFFER_SIZE / 2; i ++) { 464 | // 465 | if (y[i] > max_y) { 466 | // 467 | max_y = y[i]; 468 | max_x = i; 469 | } 470 | } 471 | max_y = max(max_y, EFFECTIVE_VERTICAL_RESOLUTION); 472 | tft.setTextColor(WHITE); 473 | tft.setTextSize(1); 474 | for (i = 1; i < SCREEN_HORIZONTAL_RESOLUTION; i ++) { 475 | // 476 | i_0 = (int)((float)i * (float)BUFFER_SIZE / (float)SCREEN_HORIZONTAL_RESOLUTION / 2.0); 477 | i_1 = (int)((float)(i + 1) * (float)BUFFER_SIZE / (float)SCREEN_HORIZONTAL_RESOLUTION / 2.0); 478 | if (hit_max) { 479 | // was in the vicinity of max 480 | i_0 = max_x; 481 | hit_max = false; 482 | } else if ((max_x <= i_1) && (i_0 <= max_x)) { 483 | // is in the vicinity of max 484 | if ((i_1 - max_x) <= (max_x - i_0)) { 485 | // 486 | hit_max = true; 487 | i_1 = max_x; 488 | } else { 489 | // 490 | i_0 = max_x; 491 | } 492 | } 493 | bk[i] = SCREEN_VERTICAL_RESOLUTION - (10 + ((float)y[i_0] / (float)max_y) * (float)(EFFECTIVE_VERTICAL_RESOLUTION - 10)); 494 | tft.drawLine( 495 | i, 496 | bk[i], 497 | i + 1, 498 | SCREEN_VERTICAL_RESOLUTION - (10 + ((float)y[i_1] / (float)max_y) * (float)(EFFECTIVE_VERTICAL_RESOLUTION - 10)), 499 | CH1_SIGNAL_COLOR); 500 | if (i % DIVISION_SIZE == 0) { 501 | // 502 | float freq = ((float)i / (float)SCREEN_HORIZONTAL_RESOLUTION * (float)DT_FS[time_base]) / 2.0; 503 | tft.setCursor(i - (freq > 100 ? 8 : 5) - (freq > (int)freq ? 4 : 0), SCREEN_VERTICAL_RESOLUTION - 7); 504 | tft.print(freq, 1); 505 | } 506 | } 507 | // clear previous stats 508 | tft.fillRect(7, 19, 150, 9, BACKGROUND_COLOR); 509 | tft.setCursor(8, 20); 510 | tft.setTextColor(WHITE); 511 | tft.setTextSize(1); 512 | String s; 513 | s = "F: "; 514 | s += (float)max_x / (float)BUFFER_SIZE * (float)DT_FS[time_base]; 515 | s += "kHz "; 516 | s += (float)20 * log10(max_y); 517 | s += "dB"; 518 | tft.print(s); 519 | 520 | } else { 521 | // display time samples 522 | uint16_t maxy = 0; 523 | uint16_t miny = ADC_RESOLUTION; 524 | uint32_t avgy = 0; 525 | for (i = 1; i < BUFFER_SIZE; i ++) { 526 | // 527 | maxy = max(maxy, data16[i]); 528 | miny = min(miny, data16[i]); 529 | avgy += data16[i]; 530 | } 531 | avgy /= BUFFER_SIZE; 532 | for (i = 0, j = 0; j < SCREEN_HORIZONTAL_RESOLUTION; i ++, j += h) { 533 | // 534 | bk[i] = SCREEN_VERTICAL_RESOLUTION - (20 + (data16[i] * ADC_SCREEN_FACTOR)); 535 | bk[i + 1] = SCREEN_VERTICAL_RESOLUTION - (20 + (data16[i + 1] * ADC_SCREEN_FACTOR)); 536 | tft.drawLine( 537 | j, 538 | bk[i], 539 | j + h, 540 | bk[i + 1], 541 | CH1_SIGNAL_COLOR); 542 | if (h > 1) tft.drawPixel(j, bk[i] - 1, GREEN); 543 | } 544 | // clear previous stats 545 | tft.fillRect(7, 19, 60, 9, BLUE); 546 | tft.setCursor(8, 20); 547 | tft.setTextColor(WHITE); 548 | tft.setTextSize(1); 549 | String s; 550 | s = "Max: "; 551 | s += (float)maxy / (float)ADC_RESOLUTION * VCC_3_3; 552 | s += "V"; 553 | tft.print(s); 554 | 555 | tft.fillRect(SCREEN_HORIZONTAL_RESOLUTION / 2 - 30, SCREEN_VERTICAL_RESOLUTION - 20, 60, 9, BLUE); 556 | tft.setCursor(SCREEN_HORIZONTAL_RESOLUTION / 2 - 29, SCREEN_VERTICAL_RESOLUTION - 19); 557 | tft.setTextColor(WHITE); 558 | tft.setTextSize(1); 559 | s = "Avg: "; 560 | s += (float)avgy / (float)ADC_RESOLUTION * VCC_3_3; 561 | s += "V"; 562 | tft.print(s); 563 | 564 | tft.fillRect(7, SCREEN_VERTICAL_RESOLUTION - 20, 60, 9, BLUE); 565 | tft.setCursor(8, SCREEN_VERTICAL_RESOLUTION - 19); 566 | tft.setTextColor(WHITE); 567 | tft.setTextSize(1); 568 | s = "Min: "; 569 | s += (float)miny / (float)ADC_RESOLUTION * VCC_3_3; 570 | s += "V"; 571 | tft.print(s); 572 | h2 = h; 573 | } 574 | 575 | state = 5; 576 | } 577 | if (state == 5) { 578 | // 579 | if (bTitleChange) { 580 | // title change 581 | bTitleChange = false; 582 | tft.fillRect(0, 0, SCREEN_HORIZONTAL_RESOLUTION, 12, CH1_SIGNAL_COLOR); 583 | tft.setCursor(8, 3); 584 | tft.setTextColor(BLUE); 585 | tft.setTextSize(1); 586 | String s = "CH1 "; 587 | s += .65; 588 | s += "V "; 589 | if (trigger == 0) { 590 | // spectrum 591 | s += (int)DT_FS[time_base]; 592 | s += "kHz "; 593 | } else { 594 | // time samples 595 | s += DT_DIV[time_base]; 596 | s += "us "; 597 | } 598 | if (trigger == 1) { 599 | // raising front trigger 600 | s += "Raising "; 601 | } else if (trigger == 2) { 602 | // descending front trigger 603 | s += "Falling "; 604 | } else if (trigger == 3) { 605 | // no trigger 606 | s += "None "; 607 | } else { 608 | // spectrum scope 609 | s += "Spectrum "; 610 | } 611 | tft.print(s); 612 | if (freeze) { 613 | // 614 | tft.setCursor(170, 3); 615 | tft.setTextColor(RED); 616 | tft.setTextSize(1); 617 | tft.print("Freeze"); 618 | } 619 | tft.setCursor(215, 3); 620 | tft.setTextColor(BLACK); 621 | tft.setTextSize(1); 622 | tft.print("GameInstance.com"); 623 | } 624 | if (freeze == 3) { 625 | // 626 | freeze = 1; 627 | export_to_sd(); 628 | bScreenChange = true; 629 | } 630 | } 631 | 632 | delay(50); 633 | state = 1; 634 | } 635 | 636 | -------------------------------------------------------------------------------- /STM32_oscilloscope_schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gameinstance/STM32-Oscilloscope/3fa26fe2016edb093b8c9b1bade081f913bfc6a3/STM32_oscilloscope_schematic.png --------------------------------------------------------------------------------