PIC18F4550 Scope II: CDC bulk mode
Two years ago I blogged about the PIC18F4550 oscilloscope using the 'hid_custom' from the Microchip Library for Applications (MLA). A few weeks ago I decided to try different USB transfer modes. In this blog I will use the 'cdc_basic' example of the MLA. It uses USB Bulk transfer to transfer data from the controller to the PC. Let'see if we can build a simple oscilloscope.
I changed the cdc_basic example. I added two command:
  • start capturing adc
  • stop capturing adc on AN0
For capturing adc values on AN0 I use two interrupts:
  • timer1 interrupt for starting ADC capture (TMR1IF)
  • ADC ready interrupt (ADIF)

The captured values are stored in a buffer or actually two buffers each 640 bytes. I use a write index, a read index and a adc counter.
When writing data, the adc counter is increased, when reading (=sending data to PC) the adc counter is decreased. On overruns, the buffer the indices and counter are reset.

The code below shows the initialization of the ADC module.

void InitializeADC() { adc_counter = 0; writeIndex =0; readIndex =0; isLowBufferRead = true; isLowBufferWrite = true; /* 1. Configure the A/D module: • Configure analog pins, voltage reference and digital I/O (ADCON1) • Select A/D input channel (ADCON0) • Select A/D acquisition time (ADCON2) • Select A/D conversion clock (ADCON2) • Turn on A/D module (ADCON0) 2. Configure A/D interrupt (if desired): • Clear ADIF bit • Set ADIE bit • Set GIE bit 3. Wait the required acquisition time (if required). 4. Start conversion: • Set GO/DONE bit (ADCON0 register) 5. Wait for A/D conversion to complete, by either: • Polling for the GO/DONE bit to be cleared OR • Waiting for the A/D interrupt 6. Read A/D Result registers (ADRESH:ADRESL); • clear bit ADIF, if required. 7. For next conversion, go to step 1 or step 2, as required. The A/D conversion time per bit is defined as T AD. • A minimum wait of 3 T AD is required before the next acquisition starts. */ PIR1bits.ADIF = 0x00; PIE1bits.ADIE = 0x00; IPR1bits.ADIP = 0x00; TRISAbits.TRISA0 = 1; ADCON0 = 1; ADCON1 = 0x0E; // AN0 is analog input ADCON2bits.ADFM = 1; // ADFM : right justified ADCON2bits.ADCS2 =1; // 0b110; // 48Mhz => Tad = 64Tosc => ADCS2...0 = 110b; ADCON2bits.ADCS1 =1; // 0b110; // 48Mhz => Tad = 64Tosc => ADCS2...0 = 110b; ADCON2bits.ADCS0 =0; // 0b110; // 48Mhz => Tad = 64Tosc => ADCS2...0 = 110b; ADCON2bits.ACQT2 =1; // 0b101; 8 T AD ADCON2bits.ACQT1 =0; // 0b101; 8 T AD ADCON2bits.ACQT0 =0; // 0b101; 8 T AD ADCON0bits.ADON = 1; // Turn on A/D module (ADCON0) PIR1bits.ADIF = 0; //Clear ADIF bit PIE1bits.ADIE = 1; //Set ADIE bit INTCONbits.GIEH = 1; // Set GIE bit ADCON0bits.GO = 1; }

Initializing timer1:

PIE1bits.TMR1IE = 0; // Timer1 interrupt disabled PIR1bits.TMR1IF = 0; // Timer1 interrupt Flag cleared T1CONbits.TMR1ON = 0; //stop timer 1 T1CONbits.TMR1CS = 0; //Fosc /4 T1CONbits.NOT_T1SYNC = 0; //sync clock T1CONbits.T1OSCEN =0; // Timer1 oscillator is shut of //T1CONbits.T1CKPS = 0b11; // prescalar 8 T1CONbits.T1CKPS = 0b00; // no prescalar 8 TMR1L = timer1_low; // Reset Timer1 lower 8 bits TMR1H = timer1_high; // Reset Timer1 upper 8 bits T1CONbits.TMR1ON = 1; //start timer 1 PIE1bits.TMR1IE = 1; // Timer1 interrupt enabled PIR1bits.TMR1IF = 0x00; // Timer1 interrupt Flag cleared

The storing of the ADC values:

void APP_DeviceReadPot() { if(isRunning) { if(writeIndex > adc_buffer_size - 2 || writeIndex < 0) { writeIndex = 0; isLowBufferWrite = !isLowBufferWrite; } if(isLowBufferWrite) { adcBuffer[writeIndex++] = ADRESH; //Measured analog voltage MSB adcBuffer[writeIndex++] = ADRESL; //Measured analog voltage LSB } else { adcBuffer2[writeIndex++] = ADRESH; //Measured analog voltage MSB adcBuffer2[writeIndex++] = ADRESL; //Measured analog voltage LSB } adc_counter += 2; } }

Below you can see the adjusted APP_DeviceCDCBasicDemoTasks which transfers the data to the PC.

void APP_DeviceCDCBasicDemoTasks() { /* Make sure that the CDC driver is ready for a transmission. */ if(mUSBUSARTIsTxTrfReady() == true) { int bytesToRead = adc_counter; uint_fast16_t i=0; if(bytesToRead > adc_buffer_size+adc_buffer_size) { readIndex=0; writeIndex =0; adc_counter=0; } else if(bytesToRead>0 && isRunning) { if(readIndex == adc_buffer_size) { readIndex =0; isLowBufferRead = !isLowBufferRead; } uint8_t* pBuffer; if(isLowBufferRead) { pBuffer= &adcBuffer[readIndex]; } else { pBuffer= &adcBuffer2[readIndex]; } if(readIndex + bytesToRead < adc_buffer_size) { putUSBUSART(pBuffer, bytesToRead); readIndex+= bytesToRead; } else { bytesToRead = adc_buffer_size - readIndex; if(bytesToRead >0) { putUSBUSART(pBuffer, bytesToRead); } readIndex=0; } adc_counter -= bytesToRead; } } ... }

I adjusted the 'Dynamic CDC Demo' c++ code as such that it sends the corresponding commands 1=start adc and 0=stop adc. It reads the adc bulkdata and stores it in a 1900 bytes buffer and displays the data graphically. You can scale and offset on x-axis. Below you can see the captured output of a 555 timer in astable mode: C1= 100nF, R1 = 10k and R2 = 10k. With 10 procent tolerance the frequency is between 397.521 and 593.828Hz, but approximately/ideally 481 Hz.
Capturing 555 timer output with CDC usb.
Capturing 555 timer output with CDC usb.

The average is 21515.04 ADC/second. Tsample = 0.046479201 msec. As you can see it has 44 points for one cycle = 2.045084825 msec. Which is a frequency of 488.98 Hz. Not bad I'd say
Compared to the previous USB transfer mode (polling) the real-time capturing has been improved a lot: 20k+ samples/second instead of 500 samples/second. Which means an improvment of 4000% or a factor of 40!


The sources of the PIC18F4550, the hex file and the windows Oscilloscope program can be downloaded here:

Back to List

All form fields are required.
A confirmation mail for the comments will be send to you.