Карманный осциллограф на микроконтроллере STC 8051

в 9:00, , рубрики: Без рубрики
Карманный осциллограф на микроконтроллере STC 8051 - 1

Привет! Габариты этой самоделки в модном корпусе из оргстекла на латунных стойках составляют всего 57x40x26 мм, и то, если учитывать выступающую ручку управления, рычажок микротумблера и разъём питания Micro USB.

Предусмотрена цифровая индикация частоты и амплитуды, а также генераторы стандартных сигналов — синусоиды и меандра. Получился неплохой карманный пробник, который выручит во многих ситуациях.

Набор для сборки такого осциллографа может стать отличным подарком гику, если он способен припаять на печатную плату микросхему в квадратном корпусе SOT389-2 (LQFP44) с шагом выводов 0.8 мм.

Карманный осциллограф на микроконтроллере STC 8051 - 2

▍ Схема питания

Принципиальная схема устройства не содержит ничего особенного. Микроконтроллер питается пятью вольтами напрямую от разъёма Micro USB. Это возможно благодаря тому, что для питания ядра и периферии микроконтроллера предусмотрен встроенный стабилизатор напряжения.

Карманный осциллограф на микроконтроллере STC 8051 - 3

Микроконтроллер может функционировать в диапазоне напряжений питания от 2 до 5.5 вольт, но 0.96-дюймовому OLED-экранчику требуется не менее 2.8 В. То есть возможно питание от трёх полуторавольтовых элементов АА или ААА, либо от литиевого аккумулятора.

Посадочное место с отверстиями для пайки проводов от батареи или аккумулятора предусмотрено, но просто соединено параллельно с разъёмом Micro USB. Иными словами, если подключить внешнее питание, на клеммы химического источника тока попадёт пять вольт. И это, мягко говоря, весьма нежелательно.

Для сравнения, на плате Arduino Uno R3 имеется узел на компараторе и P-канальном полевом транзисторе, отключающий линию +5 вольт разъёма USB-B при наличии внешнего питания на коаксиальном разъёме.

Карманный осциллограф на микроконтроллере STC 8051 - 4

В некоторых дешёвых устройствах из Поднебесной замечено более простое решение, когда литиевый аккумулятор подключается к плюсовой шине питания просто через резистор.

Разработчики надеются, что встроенная плата защиты аккумулятора ограничит зарядный ток и отключит заряд при достижении максимального допустимого напряжения.

Это работает, только не следует забывать, что литиевый аккумулятор является источником повышенной опасности, в отношении которого верно следующее:

  1. плата защиты является не контроллером заряда, а лишь ограничителем предельно допустимых значений напряжения, тока и температуры,
  2. платы защиты бывают разные и могут не обеспечивать всех необходимых ограничений,
  3. не у каждого литиевого аккумулятора имеется плата защиты.

В более серьёзном устройстве не помешало бы добавить супрессоры для защиты входов от перенапряжения. А на разъём питания, кроме того, и самовосстанавливающийся предохранитель.

▍ Аналого-цифровое преобразование

Входной сигнал поступает на микроконтроллер через резистивный делитель напряжения R4R5 с коэффициентом передачи 2/(10+2) = 1/6. То есть, при тридцати вольтах на входе микроконтроллеру достанется пять вольт.

Карманный осциллограф на микроконтроллере STC 8051 - 5

В таком случае ток через делитель составит 30/12 = 2.5 мА, а мощность тепловыделения на обоих резисторах, согласно закону Джоуля-Ленца, 2.5*30 = 75 милливатт. На первый взгляд, применение одноваттного металлоплёночного резистора типоразмера 2512 в качестве R4 может показаться избыточным.

Однако в профессионально спроектированных приборах большие мощные резисторы применяются во входных цепях довольно часто, и этому есть как минимум две причины:

  1. такие резисторы являются более высоковольтными и имеют лучшую устойчивость к пробою,
  2. они лучше охлаждаются, что положительно сказывается на точности измерений, ведь сопротивление токопроводящего слоя зависит от температуры.

Этот миниатюрный осциллограф является проектом с открытым исходным кодом, который можно скачать на github.

Информация об авторских правах

    Copyright (c) 2020 Creative Lau (creativelaulab@gmail.com)

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.

Делитель частоты аналого-цифрового преобразования выбирается в зависимости от выбранной частоты развёртки.

/* scale_h: Time scale 500ms, 200ms, 100ms, 50ms, 20ms, 10ms, 5ms, 2ms, 1ms, 500us, 200us, 100us */
void ADCInit(uint8 scale_h)
{
    uint8 ADC_SPEED;

    switch (scale_h)
    {
    case 0: //500ms
        ADC_SPEED = ADC_SPEED_512;
        break;

    case 1: //200ms
        ADC_SPEED = ADC_SPEED_512;
        break;

    case 2: //100ms
        ADC_SPEED = ADC_SPEED_512;
        break;

    case 3: //50ms
        ADC_SPEED = ADC_SPEED_512;
        break;

    case 4: //20ms
        ADC_SPEED = ADC_SPEED_512;
        break;

    case 5: //10ms
        ADC_SPEED = ADC_SPEED_512;
        break;

    case 6: //5ms
        ADC_SPEED = ADC_SPEED_512;
        break;

    case 7: //2ms
        ADC_SPEED = ADC_SPEED_512;
        break;

    case 8: //1ms
        ADC_SPEED = ADC_SPEED_352;
        break;

    case 9: //500us
        ADC_SPEED = ADC_SPEED_192;
        break;

    case 10: //200us
        ADC_SPEED = ADC_SPEED_32;
        break;

    case 11: //100us
        ADC_SPEED = ADC_SPEED_32;
        break;
    }
    ADCCFG = RESFMT | ADC_SPEED; //Результат выравнивается по правому краю, а тактовая частота АЦП устанавливается на системную тактовую частоту /2/16/16
    ADC_CONTR = ADC_POWER;       //Включаем модуль АЦП
    Delay5ms();                  //Задержка, чтобы АЦП успел включиться
}

Чтение результатов преобразования производится следующим образом.

uint16 ADCRead(uint8 chx)
{
    uint16 res;
    ADC_RES = 0;            //Очищаем регистры результата
    ADC_RESL = 0;           
    ADC_CONTR &= 0xF0;      //Выбор входного канала АЦП
    ADC_CONTR |= chx;
    ADC_CONTR |= ADC_START; //Запускаем преобразование
    _nop_();
    _nop_();

    while (!(ADC_CONTR & ADC_FLAG))
        ; //Флаг завершения преобразования

    ADC_CONTR &= ~ADC_FLAG;          
    res = (ADC_RES << 8) | ADC_RESL; //Чтение результатов АЦП
    return res;
}

По опыту сборки цифровых вольтметров мы знаем, что для измерения внешнего напряжения необходим источник опорного напряжения (ИОН).

На схеме нашего осциллографа он отсутствует, зато имеется встроенный внутри микроконтроллера. Если измерить модулем АЦП напряжение этого источника, то мы будем знать напряжение питания микроконтроллера, исходя из которого сможем рассчитать напряжение входного сигнала.

uint16 GetADC_CHX(uint8 chx)
{   uint16 ADCx;
    uint8 i;

    ADCInit(0); //Инициализация АЦП

    //Измеряем напряжение на встроенном источнике опорного напряжения
    ADCRead(chx); //Отбрасываем первые два результата, которые могут быть неточными после переключения канала АЦП
    ADCRead(chx);
    ADCx = 0;

    for (i = 0; i < 16; i++)
    {
        ADCx += ADCRead(chx);
    }

    ADCx >>= 4; //Вычисляем среднее арифметическое
    return ADCx;
}

Теперь мы можем измерять мгновенные значения напряжения, исходя из которых будем строить осциллограмму на экране.

uint16 GetVoltage(uint8 chx, uint16 lsb)
{
    uint16 ADCbg;
    uint16 *BGV;
    uint16 ADCx;
    uint16 Vx;

    ADCInit(0); 

    //Получаем усреднённое значение напряжения ИОН через 16-й канал АЦП
    ADCbg = GetADC_CHX(ADC_CHS_BG);

    //Получаем усреднённое значение входного напряжения ИОН через канал chx
    ADCx = GetADC_CHX(chx);

    //Рассчитываем входное напряжение
    BGV = GetBGV();
    Vx = (uint32)*BGV * ADCx * lsb / ADCbg / 100;
    return Vx;
}

//	ADCRead(chx) Timing:500ms, 200ms, 100ms, 50ms, 20ms, 10ms, 5ms, 2ms, 1ms, 500us, 200us, 100us
//	Горизонтальное разрешение равно 100 точкам - 4 группам по 25
//  24MHz
//	ADC_SPEED_512 28us
//	ADC_SPEED_480 26.2us
//	ADC_SPEED_448 25us
//	ADC_SPEED_416 23.8us
//	ADC_SPEED_384 22us
//	ADC_SPEED_352 20us
//	ADC_SPEED_320 18us
//	ADC_SPEED_288 17us
//	ADC_SPEED_256 15us
//	ADC_SPEED_224 14us
//	ADC_SPEED_192 12us
//	ADC_SPEED_160 10.4us
//	ADC_SPEED_128 9us
//	ADC_SPEED_96 7us
//	ADC_SPEED_64 5.6us
//	ADC_SPEED_32 4us
//
//  27MHz
//	ADC_SPEED_512 26us
//	ADC_SPEED_352 19us
//	ADC_SPEED_192 11us
//	ADC_SPEED_32 4us
void switch_Dealy(uint8 scale_h)
{
    switch (scale_h)
    {
        //500ms ADC_SPEED_512
    case 0:
        Delay19971us();
        break;

        //200ms ADC_SPEED_512
    case 1:
        Delay7971us();
        break;

        //100ms	ADC_SPEED_512
    case 2:
        Delay3971us();
        break;

        //50ms ADC_SPEED_512
    case 3:
        Delay1971us();
        break;

        //20ms ADC_SPEED_512
    case 4:
        Delay771us();
        break;

        //10ms ADC_SPEED_512
    case 5:
        Delay371us();
        break;

        //5ms ADC_SPEED_512
    case 6:
        Delay171us();
        break;

        //2ms ADC_SPEED_512
    case 7:
        Delay51us();
        break;

        //1ms ADC_SPEED_352
    case 8:
        Delay18us();
        break;

        //500us	ADC_SPEED_192
    case 9:
        Delay6us();
        break;

        //200us ADC_SPEED_32
    case 10:
        /* Точная настройка интервала выборки */
        _nop_();
        //_nop_();
        break;

        //100us ADC_SPEED_32
    case 11:

        break;
    }
}

▍ Синхронизация и развёртка

Чтобы форма волны на экране представляла собой стабильную осциллограмму, а не ползла вбок и не превращалась в сетку переплетённых копий, необходима синхронизация, которая запускается при достижении мгновенным значением входного напряжения заданного пользователем порога.

Достигнуть того или иного значения можно как сверху, так и снизу. Поэтому предусмотрен выбор режима синхронизации — по подъёму или спаду входного напряжения.

Развёртка может срабатывать однократно, фиксируя осциллограмму до ручного перезапуска, либо автоматически перезапускаться двумя способами: после каждого заполнения буфера для периодических сигналов, либо после срабатывания синхронизации для апериодических или редко повторяющихся.

Нижеприведённый код заполняет кольцевой буфер, куда записываются мгновенные значения входного напряжения, с учётом настроек развёртки и синхронизации.

uint16 *GetWaveADC(uint8 chx, uint8 scale_h)
{
    uint8 i, j;
    static uint16 ADCSampling[SAMPLE_NUM];
    uint16 ADCPreSampling[PRE_BUF_NUM + 1]; //Первая дополнительная позиция используется для копирования значения последней позиции при формировании циклического кэша.

    ADCComplete = 0; //Снимаем флаг завершения выборки
    if (ADCInterrupt)
        return ADCSampling;
    memset(ADCSampling, 0x00, SAMPLE_NUM * 2);
    memset(ADCPreSampling, 0x00, (PRE_BUF_NUM + 1) * 2);
    //BGV = GetBGV(); 
    ADCbg = GetADC_CHX(ADC_CHS_BG);                         //Получаем значение опорного напряжения
    TriggerADC = Convert_mv_ADC(TriLevel, BGV, ADCbg, Lsb); //Преобразовываем заданное пользователем значение порога синхронизации в значение АЦП

    //Ждём стабилизации напряжения ИОН
    ADCInit(scale_h);
    ADCRead(chx); //Отбрасываем первые два результата
    ADCRead(chx);

    //Минимальный временной интервал 100 микросекунд не поддерживает однократную развёртку
    if (scale_h == 11) //100 us
    {
        P_Ready = 1; //Начинаем фиксацию мгновенных значений и зажигаем светодиод — индикатор синхронизации
        for (i = 0; i < SAMPLE_NUM; i++)
        {
            if (ADCInterrupt)
                return ADCSampling;
            ADCSampling[i] = ADCRead(chx);
#ifdef DEBUG
            P15 = ~P15;
#endif
        }
        P_Ready = 0; //После заполнения буфера гасим индикатор синхронизации
    }

    /* Выбор режима: однократная или повторяющаяся развёртка*/
    else if (TriMode)
    {
        P_Ready = 0;                       //Предварительно заполняем буфер, не реагируя на порог синхронизации, чтобы не потерять левую часть осциллограммы
        for (j = 1; j <= PRE_BUF_NUM; j++) 
        {
            if (ADCInterrupt)
                return ADCSampling;

            Delay3us();            //Коррекция временного интервала: цикл выборки при однократной развёртке или первом запуске непрерывной на 3 мкс медленнее, чем при автоматическом перезапуске.
            switch_Dealy(scale_h); //Задержка выборки
            ADCPreSampling[j] = ADCRead(chx);
#ifdef DEBUG
            P15 = ~P15;
#endif
        }
        P_Ready = 1; //Когда предварительное кэширование завершено, загорается индикатор и можно обрабатывать порог синхронизации.

        //Цикл кэширует точки выборки PRE_BUF_NUM перед запуском и определяет, соответствуют ли точки выборки условиям запуска.
        while (1)
        {
            if (ADCInterrupt)
                return ADCSampling;

            //Если предварительный буфер переполняется, то последнее значение выборки копируется в первую позицию.
            if (j > PRE_BUF_NUM)
            {
                j = 1;
                ADCPreSampling[0] = ADCPreSampling[PRE_BUF_NUM];
            }
            switch_Dealy(scale_h); //Задержка выборки
            ADCPreSampling[j] = ADCRead(chx);
            if (GetTriggerPos(ADCPreSampling[j - 1], ADCPreSampling[j], TriggerADC, TriSlope)) //Соблюдаются ли условия срабатывания триггера синхронизации?
            {
                P_Ready = 0; //После успешной синхронизации выходим из цикла while и гасим светодиод
                break;
            }
            j++;
#ifdef DEBUG
            P15 = ~P15;
#endif
        }

        //Продолжаем запоминание мгновенных значений входного напряжения
        for (i = 0; i < AFT_BUF_NUM; i++)
        {
            if (ADCInterrupt)
                return ADCSampling;

            Delay3us();            
            switch_Dealy(scale_h); 
            ADCSampling[i + PRE_BUF_NUM] = ADCRead(chx);
#ifdef DEBUG
            P15 = ~P15;
#endif
        }

        //Объединяем первую и последнюю точки выборки PRE_BUF_NUM в полную форму волны.
        for (i = 0; i < PRE_BUF_NUM; i++) //Первое и последнее значения выборки в предварительном кэше равны, первое значение отбрасываем, а оставшиеся точки выборки PRE_BUF_NUM-1 сортируем в порядке выборки как первые точки выборки PRE_BUF_NUM-1 ADCSampling.
        {
            if (ADCInterrupt)
                return ADCSampling;
            if (++j > PRE_BUF_NUM) //Переполнение предварительного кэша
                j = 1;

            ADCSampling[i] = ADCPreSampling[j];
        }
    }

    /* Режим повторяющейся (автоматической) развёртки */
    else
    {
        P_Ready = 1; //Начинаем отбор мгновенных значений и зажигаем индикатор
        for (i = 0; i < SAMPLE_NUM; i++)
        {
            if (ADCInterrupt)
                return ADCSampling;
            ADCSampling[i] = ADCRead(chx);
            Delay3us();            //Коррекция временного интервала
            switch_Dealy(scale_h); //Задержка выборки
#ifdef DEBUG
            P15 = ~P15;
#endif
        }
        P_Ready = 0; //Гасим светодиод
    }
    ADCComplete = 1; //Устанавливаем флаг завершения выборки
    return ADCSampling;
}

#endif

▍ Генератор стандартных сигналов

Тестовый меандр формируется предельно простым способом: при инициализации программного обеспечения осциллографа запускается модуль ШИМ, относящийся, как и модуль АЦП, к разряду периферии, независимой от ядра.

Преобразование меандра в синусоиду осуществляется посредством пассивного RLC-фильтра. При отсутствии резистора RP1 граничная частота фильтра L1C3 равнялась бы 34 килогерцам при характеристическом сопротивлении 213 Ом.

Карманный осциллограф на микроконтроллере STC 8051 - 6

Однако последовательно с катушкой имеется подстроечный резистор, в верхнем (по схеме) положении которого частота среза составила бы 36 герц даже при отсутствии индуктивности. Итак, мы можем регулировать граничную частоту этого фильтра нижних частот в очень широких пределах, наблюдая за изменением осциллограммы выходного сигнала. Получается хорошая синусоида.

Карманный осциллограф на микроконтроллере STC 8051 - 7

▍ Итоги работы

Собранный радиоконструктор начинает адекватно работать сразу при первом включении и позволяет наблюдать форму волны в пределах частотного диапазона слышимых звуков, а также графики более медленных электрических колебаний.

Видео: карманный осциллограф

Напишите в комментариях, какие усовершенствования целесообразно внести в этот карманный осциллографический пробник.

Автор: Гитарная электроника

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js