Главная » Программирование » PIC - микроконтроллеры » ЦАП на PIC18F с использованием ШИМ и RC цепочки
ЦАП на PIC18F с использованием ШИМ и RC цепочки

ЦАП на PIC18F с использованием ШИМ и RC цепочки

Цикл статей – PIC начинающим или основы основ

ЦАП на RC-цепочке и ШИМ

Далеко не во всех микроконтроллерах (МК) имеется цифро-аналоговый преобразователь (ЦАП). Как же исправить эту ситуацию не прибегая к сторонним микросхемам? Очень просто! Особенно, если не нужна большая точность и скорость =) Далее под катом…

Немного теории про RC-цепь и ШИМ

ЦАП
Цифро аналоговый преобразователь (ЦАП), на английском Digital-to-analog converter (DAC). ЦАП предназначен для преобразования цифрового кода в аналоговый сигнал. Хотя RC-цепь преобразует не цифровой код, а ШИМ в аналоговый сигнал… Но кого интересуют такие мелочи? 🙂
ШИМ
Широтно-импульсная модуляция (ШИМ), на английском Pulse-width modulation (PWM). ШИМ — изменение скважности импульсов для соответствующего изменения напряжения на нагрузке.

В основе идеи лежат интегрирующие свойства RC цепочки, или тот факт, что RC-цепь — это фильтр нижних частот. На рисунках ниже продемонстрирован этот эффект, в качестве примера выбрана RC цепь, в которой R=1 кОм, С=0,1 мкФ.

Видно, что чем выше частота импульсов, тем более напряжение на конденсаторе C похоже на постоянное. Это означает, что для уменьшения пульсаций нужно либо увеличивать постоянную времени RC-цепочки, либо увеличивать частоту следования импульсов. Но при увеличении постоянной RC-цепи увеличивается и время, требующееся для установления процесса. Вот этот эффект:

Значение постоянного напряжения в установившемся режиме зависит от напряжения импульсов и от коэффициента заполнения ШИМ. Ниже показано как зависит:

Если к конденсатору C ничего не подключено, уровень напряжения на нем будет равен Uимп * Kзаполнения.

Практика — ЦАП и PIC18F45K20

Для экспериментов нужно доработать макетку: добавить резистор 10 кОм, 2 конденсатора по 0,1 мкФ. Подсоединить как показано на рисунке:

Схема доработки для ЦАП

Схема доработки для ЦАП

Тест для проверки

Для проверки работоспособности схемы нужно измерить напряжение V0 подавая на резистор меандр. Напряжение V0 должно быть равно примерно половине напряжения питания МК.

Простенькая программа для генерации меандра для схемы состоит всего из нескольких строчек:

/*
 * File:   main_dac.c
 * Author: PRO-DIOD.RU
 *
 */

#include <xc.h> // В этом проекте можно заменить строкой #include <pic18f45k20.h>
#include "config_bits.h"

/*######################################################################################################################
 * Г Л А В Н А Я   Ф У Н К Ц И Я
 */
int main(void)
{
/* Включаем генератор */
OSCCON = 0b01110001; // Internal; 16MHz;
OSCTUNEbits.PLLEN = 1; // PLL включен! 16MHz * 4 = 64MHz;

/* Настройка портов I/O */
TRISD = 0x00;// Порт D как выход
LATD = 0x00; // Гасим все светодиоды на PORTD
LATEbits.LATE1 = 0; // I/O для АЦП
TRISEbits.TRISE1 = 0; // E1 как выход (для АЦП)

TRISB = 0x00000001; // RB0 как вход (для кнопки)

while(1)
  {
	LATEbits.LATE1 = !LATEbits.LATE1; // Делаем меандр на E1
  } // while(1) end
} // main end

Проект для тестирования меандром

Тест ЦАП (меандр)
Тест ЦАП (меандр)
project_dac_rc_1.zip
134.5 KiB
57 Downloads
Детали

У меня получилось так:

Результат работы RC-цепочки при меандре

Результат работы RC-цепочки при меандре

Рабочий вариант ЦАП

В примере выше, динамичный и требующий точного времени процесс крутится в основном цикле, что сводит на нет дальнейшее развитие программы, т.к. любое изменение в коде приведет к «уплыванию» параметров ШИМ. Поэтому необходимо реализовать все это как-то иначе. Я вижу два способа реализовать задуманное — через «заднее место» и «как у людей». Начнем, разумеется, через задницу, а именно на таймере и нескольких переменных, которые обрабатываются в прерывании таймера.

Программный ШИМ на таймере TMR2

В PIC18F45K20 имеется четыре таймера (TMR0, TMR1, TMR2 [PR2], TMR3). Будем использовать TMR2. Это 8-ми битный таймер, с предделителем имеющим значения {1:1; 1:4; 1:16} и постделителем, имеющим значения {1:1; 1:2; … 1:15; 1:16}. Таймер тактируется основной частотой деленной на четыре (в нашем случае 64 МГц / 4 = 16 МГц). В статье про прерывания я уже показывал как работать с таймером TMR1, работа с таймером 2 в целом аналогична: Нужно выставить значения делителей, включить прерывания, разрешить работу таймера. При настройке таймера я выбрал частоту ШИМ 10 кГц, считаю это подходящим компромиссом между пульсациями в десятые доли вольта и частотой вызова прерываний таймером. У таймера 2 имеется регистр RP2, которым можно задавать частоту срабатывания таймера. Код настройки таймера:

/* Настройка таймера таймера 2 */
PIE1bits.TMR2IE     = 1; // Включить прерывание (interrupt) TMR1
IPR1bits.TMR2IP     = 1; // Установить высокий приоритет для прерывания TMR1
T2CONbits.T2CKPS    = 2; // Prescaler = 0b00000010 (1:16)
T2CONbits.T2OUTPS   = 0; // Postscaler= 0b00000000 (1:1)
PR2 = 100; // 64M / 4 / 16 / 1 / 100 = 10000Hz
TMR2ON = 1; // Старт таймера

В функции прерывания напишем обработчик таймера:

/*######################################################################################################################
 * ОБРАБОТЧИК ПРЕРЫВАНИЙ с ВЫСОКИМ ПРИОРИТЕТОМ
 */
void interrupt high_isr (void)
{

if (PIR1bits.TMR2IF && PIE1bits.TMR2IE) // Обработали прерывание от TMR2 (Таймер 2)
  {
    pwm_n = 100 - pwm_p; // Вычисляем длительность отрицательного полупериода импульса
    if (pwm_phase) // Если кончился положительный полупериод нужно подготовить и запустить отрицательный полупериод
      {
        LATEbits.LATE1 = 0;
        PR2 = pwm_n;
        pwm_phase = 0; // Начинаем отрицательную
      }else // Если кончился отрицательный полупериод нужно подготовить и запустить положительный полупериод
      {
        LATEbits.LATE1 = 1;
        PR2 = pwm_p;
        pwm_phase = 1;
      }

    PIR1bits.TMR2IF = 0; // Сбросили флаг прерывания по INT0
  } // if (PIR1bits.TMR2IF && PIE1bits.TMR2IE)

} // high_isr END

Обратите внимание на переменные pwm_phase, pwm_p, pwm_n — это глобальные переменные, они объявляются не в функции, а в начале файла:

/*
 * File:   main_dac.c
 * Author: PRO-DIOD.RU
 *
 */

#include <xc.h> // В этом проекте можно заменить строкой #include <pic18f45k20.h>
#include "config_bits.h"

/*######################################################################################################################
 * Глобальные переменные
 */
unsigned char pwm_p = 10; // Длительность положительного полупериода (0..100), необходимо задавать
unsigned char pwm_n = 0; // Длительность отрицательного полупериода (0..100), вычисляется автоматически
unsigned char pwm_phase = 0; // Фаза периода - 0=0; 1=1

    ...
	...

Событие по таймеру вызывается два раза за период — когда заканчивается положительный полупериод ШИМ и когда заканчивается отрицательный полупериод ШИМ. За признак фазы (признак полупериода) отвечает переменная pwm_phase, которая принимает значения 0 или 1. Таймер настроен таким образом, чтобы при значении в счетчика PR2 = 100 время таймера равно 100 мкс (10 кГц), это время делится между отрицательными и положительным полупериодом. За отрицательный полупериод отвечает переменная pwm_n, за положительный — pwm_p. Все сказанное поясняется на рисунке:

ШИМ (PWM) на таймере

ШИМ (PWM) на таймере

И все бы было хорошо, но код обработки таймера занимает некоторое время, что при слишком маленьком или слишком большом значении pwm_p существенно влияет на значения ШИМ. Реально, значения pwm_p имеют смысл в интервале 5..95, а более-менее хорошая линейность ШИМ обеспечивается в диапазоне значений pwm_p 15..85. Из-за этого же «съедаются» такты таймера и частота получается не 10 кГц, а чуть меньше. Подробности на скриншотах USB-осциллографа:

Проект в котором реализован тест.

Тест ЦАП, ШИМ меняется переменной pwm_p
91.1 KiB
50 Downloads
Детали

Для изменения параметров ШИМ необходимо менять значения переменной pwm_p в строке 13. Напоминаю, что значения pwm_p нужно менять в пределах 5..95, иначе программа будет работать некорректно.

Впрочем, приведенный пример ни фига не интересный, поэтому допишем программу так, чтобы уровень ШИМ можно было менять кнопкой SW1 и заодно визуализировать сие действо, для этого будем использовать имеющиеся восемь светодиодов.

Краткое описание программы. Начальное состояние — ШИМ установлен на 10% (pwm_p = 10), светодиоды не светятся. Каждое нажатие на кнопку приводит к увеличению ШИМ на 10% (pwm_p = pwm_p + 10) и поочередно зажигаются светодиоды:

ШИМ на кнопке

ШИМ на кнопке

Опрос кнопок сначала реализовал на таймере 2, но как показала практика, 10 кГц оказалось достаточно для отлова дребезга, поэтому не заморачиваясь всякими программными счетчиками реализовал опрос кнопок на таймере 1 настроенном на частоту 50 Гц. Вот код главного си файла:

/*
 * File: main_dac.c
 * Author: PRO-DIOD.RU
 *
 */

#include <xc.h> // В этом проекте можно заменить строкой #include <pic18f45k20.h>
#include "config_bits.h"

/*######################################################################################################################
 * Глобальные переменные
 */
// Переменные для ШИМ
unsigned char pwm_p = 10; // Длительность положительного полупериода (0..100), необходимо задавать
unsigned char pwm_n = 0; // Длительность отрицательного полупериода (0..100), вычисляется автоматически
unsigned char pwm_phase = 0; // Фаза периода - 0=0; 1=1

// Переменные для кнопки
unsigned char but_ok = 0; // Признак нажатия кнопки:
 // 0 - не нажата
 // 1 - нажата, не обработана
 // 2 - нажата, но уже была обработана

/*######################################################################################################################
 * Г Л А В Н А Я Ф У Н К Ц И Я
 */
int main(void)
{
/* Переменные. Действуют внутри функции main() */
unsigned char but_click = 0; // Номер нажатия на кнопку

/* Включаем генератор */
OSCCON = 0b01110001; // Internal; 16MHz;
OSCTUNEbits.PLLEN = 1; // PLL включен! 16MHz * 4 = 64MHz;

/* Настройка таймера таймера 2 для ШИМ */
PIE1bits.TMR2IE = 1; // Включить прерывание (interrupt) TMR1
IPR1bits.TMR2IP = 1; // Установить высокий приоритет для прерывания TMR1
T2CONbits.T2CKPS = 2; // Prescaler = 0b00000010 (1:16)
T2CONbits.T2OUTPS = 0; // Postscaler= 0b00000000 (1:1)
PR2 = 100; // 64M / 4 / 16 / 1 / 100 = 10000Hz
T2CONbits.TMR2ON = 1; // Старт таймера

/* Настройка таймера таймера 1 для опроса кнопок */
PIE1bits.TMR1IE = 1; // Включить прерывание (interrupt) TMR1
IPR1bits.TMR1IP = 0; // Установить низкий приоритет для прерывания TMR1
T1CONbits.T1CKPS = 3; // Prescaler = 0b00000011 (1:8)
TMR1 = 25536; // 64M / 4 / 8 / (65536 - 25536) = 50Hz
T1CONbits.TMR1ON = 1; // Старт таймера

/* Настройки прерываний */
INTCONbits.PEIE = 1; // Периферийные прерывания разрешены
INTCONbits.GIE = 1; // Глобальные прерывания разрешены
RCONbits.IPEN = 1; // Разрешить двуприоритетные прерывания

/* Настройка портов I/O */
TRISD = 0x00;// Порт D как выход
LATD = 0x00; // Гасим все светодиоды на PORTD
LATEbits.LATE1 = 0; // I/O для АЦП
TRISEbits.TRISE1 = 0; // E1 как выход (для АЦП)

TRISB = 0x00000001; // RB0 как вход (для кнопки)

while(1)
 {
 if (but_ok == 1)
 {
 if (but_click < 8)
 {
 but_click++;
 }else
 {
 but_click = 0;
 }
 but_ok = 2; // Кнопка обработана
 }
 if (but_click == 0) {LATD = 0b00000000; pwm_p = 10;}
 if (but_click == 1) {LATD = 0b00000001; pwm_p = 20;}
 if (but_click == 2) {LATD = 0b00000011; pwm_p = 30;}
 if (but_click == 3) {LATD = 0b00000111; pwm_p = 40;}
 if (but_click == 4) {LATD = 0b00001111; pwm_p = 50;}
 if (but_click == 5) {LATD = 0b00011111; pwm_p = 60;}
 if (but_click == 6) {LATD = 0b00111111; pwm_p = 70;}
 if (but_click == 7) {LATD = 0b01111111; pwm_p = 80;}
 if (but_click == 8) {LATD = 0b11111111; pwm_p = 90;}
 } // while(1) end
} // main end

/*######################################################################################################################
 * ОБРАБОТЧИК ПРЕРЫВАНИЙ с НИЗКИМ ПРИОРИТЕТОМ
 */
void interrupt low_priority low_isr (void)
{
if (PIR1bits.TMR1IF && PIE1bits.TMR1IE) // Обработали прерывание от TMR1 (Таймер 1)
 {
 PIR1bits.TMR1IF = 0; // Сбросили флаг прерывания
 TMR1 = 25536; // 64M / 4 / 8 / (65536 - 25536) = 50Hz
 // Сканируем кнопку
 if ((but_ok == 0) && (!PORTBbits.RB0))
 {
 but_ok = 1;
 }
 if ((but_ok == 2) && (PORTBbits.RB0))
 {
 but_ok = 0;
 }
 } // if (PIR1bits.TMR1IF && PIE1bits.TMR1IE)
} // low_isr END

/*######################################################################################################################
 * ОБРАБОТЧИК ПРЕРЫВАНИЙ с ВЫСОКИМ ПРИОРИТЕТОМ
 */
void interrupt high_isr (void)
{
if (PIR1bits.TMR2IF && PIE1bits.TMR2IE) // Обработали прерывание от TMR2 (Таймер 2)
 {
 pwm_n = 100 - pwm_p; // Вычисляем длительность отрицательного полупериода импульса
 if (pwm_phase) // Если кончился положительный полупериод нужно подготовить и запустить отрицательный полупериод
 {
 LATEbits.LATE1 = 0;
 PR2 = pwm_n;
 pwm_phase = 0; // Начинаем отрицательную
 }else // Если кончился отрицательный полупериод нужно подготовить и запустить положительный полупериод
 {
 LATEbits.LATE1 = 1;
 PR2 = pwm_p;
 pwm_phase = 1;
 }
 PIR1bits.TMR2IF = 0; // Сбросили флаг прерывания
 } // if (PIR1bits.TMR2IF && PIE1bits.TMR2IE)
} // high_isr END

Вот осциллограмма программы. Каждое изменение аналогового сигнала — это результат нажатия на кнопку:

ЦАП из ШИМ по кнопке

ЦАП из ШИМ по кнопке

Скачать проект по ссылке:

ЦАП из ШИМ по кнопке
100.6 KiB
47 Downloads
Детали

Аппаратный ШИМ на функции PWM модуля CCP (ECCP)

Способ формирования ШИМ на прерывании таймера — это способ через «то самое место», ведь приходится 10000 (десять тысяч!!!) раз в секунду прерывать программу и выполнять рутинные операции в обработчике таймера. А ведь есть другие источники прерываний… Пришла пора сделать все как у людей. У рассматриваемого микроконтроллера PIC18F45xx, как и у многих других МК, имеется замечательный модуль — CCP.

CCP
CCP — Capture/Compare/PWM (Захват/Сравнение/ШИМ). Модуль используется для управления двигателями, построения импульсных источников питания, получения простого ШИМ для решения различных задач. В нашем PIC имеется два модуля CPP: CPP1 (ECCP) — модуль CPP с расширенной функциональностью и модуль попроще — CCP2.

Так сложилось исторически, что работать будем с модулем CCP1. Для этого резистор RC-цепочки нужно отключить от порта E1 (пин 26) и подключить к C2 (пин 36) МК.

Схема переделки для ШИМ из CCP

Схема переделки для ШИМ из CCP

Модуль CCP в режиме PWM использует таймер 2, но его прерывания при этом не нужны. Биты T2CONbits.T2CKPS определяют предварительное деление частоты для таймера (на 1, 4 или 16). Регистр PR2 задает длительность периода. Регистр CCPR1L модуля CCP отвечает за длительность положительного полупериода в ШИМ. Биты CCP1CONbits.CCP1M отвечают за режим работы модуля CCP. Регистр PR2 я принял равным 99, предделитель отключил, частота таймера при этом примрно 161 кГц — в 16 раз больше чем в примере с ШИМ на таймере :). При этом регистр значения записанные в регистр CCPR1L должны быть от 1 до 99, что очень хорошо совпадает с процентным заполнением ШИМ, т.е. CCPR1L = 25 даст ШИМ с заполнением 25%. Код основного файла:

/*
 * File:   main_dac.c
 * Author: PRO-DIOD.RU
 *
 */

#include <xc.h> // В этом проекте можно заменить строкой #include <pic18f45k20.h>
#include "config_bits.h"

/*######################################################################################################################
 * Глобальные переменные
 */
// Переменные для кнопки
unsigned char but_ok = 0;   // Признак нажатия кнопки:
                            // 0 - не нажата
                            // 1 - нажата, не обработана
                            // 2 - нажата, но уже была обработана

/*######################################################################################################################
 * Г Л А В Н А Я   Ф У Н К Ц И Я
 */
int main(void)
{
/* Переменные. Действуют внутри функции main() */
unsigned char but_click = 0; // Номер нажатия на кнопку

/* Включаем генератор */
OSCCON = 0b01110001; // Internal; 16MHz;
OSCTUNEbits.PLLEN = 1; // PLL включен! 16MHz * 4 = 64MHz;

/* Настройка таймера таймера 2 для CCP */
//PIE1bits.TMR2IE     = 1; // Включить прерывание (interrupt) TMR1
//IPR1bits.TMR2IP     = 1; // Установить высокий приоритет для прерывания TMR1
T2CONbits.T2CKPS    = 0; // Prescaler = 0b00000000 (1:1)
T2CONbits.T2OUTPS   = 0; // Postscaler= 0b00000000 (1:1)
PR2 = 99; // 64M / 4 / 1 / 1 / 99 = 161616Hz (161.6 кГц)
T2CONbits.TMR2ON = 1; // Старт таймера

/* Настройка CCP1 */
CCP1CONbits.CCP1M = 0b00001100; // режим PWM
CCPR1L = 20; // При текущей настройке таймера 2 - это процент заполнения ШИМ

/* Настройка таймера таймера 1 для опроса кнопок */
PIE1bits.TMR1IE     = 1; // Включить прерывание (interrupt) TMR1
IPR1bits.TMR1IP     = 0; // Установить низкий приоритет для прерывания TMR1
T1CONbits.T1CKPS    = 3; // Prescaler = 0b00000011 (1:8)
T1CONbits.RD16      = 1; // 16-бит режим
TMR1 = 25536; // 64M / 4 / 8 / (65536 - 25536) = 50Hz
T1CONbits.TMR1ON = 1; // Старт таймера

/* Настройки прерываний */
INTCONbits.PEIE = 1;            // Периферийные прерывания разрешены
INTCONbits.GIE  = 1;            // Глобальные прерывания разрешены
RCONbits.IPEN   = 1;            // Разрешить двуприоритетные прерывания

/* Настройка портов I/O */
TRISD = 0x00;// Порт D как выход
LATD = 0x00; // Гасим все светодиоды на PORTD
LATEbits.LATE1 = 0; // I/O для АЦП
TRISEbits.TRISE1 = 0; // E1 как выход (для АЦП)

TRISCbits.RC2 = 0; // Вывод для CCP1 как выход

TRISB = 0x00000001; // RB0 как вход (для кнопки)

while(1)
  {
    if (but_ok == 1)
      {
        if (but_click < 8)
          {
            but_click++;
          }else
          {
            but_click = 0;
          }
        but_ok = 2; // Кнопка обработана
      }
    if (but_click == 0) {LATD = 0b00000000; CCPR1L = 10;}
    if (but_click == 1) {LATD = 0b00000001; CCPR1L = 20;}
    if (but_click == 2) {LATD = 0b00000011; CCPR1L = 30;}
    if (but_click == 3) {LATD = 0b00000111; CCPR1L = 40;}
    if (but_click == 4) {LATD = 0b00001111; CCPR1L = 50;}
    if (but_click == 5) {LATD = 0b00011111; CCPR1L = 60;}
    if (but_click == 6) {LATD = 0b00111111; CCPR1L = 70;}
    if (but_click == 7) {LATD = 0b01111111; CCPR1L = 80;}
    if (but_click == 8) {LATD = 0b11111111; CCPR1L = 90;}
  } // while(1) end
} // main end

/*######################################################################################################################
 * ОБРАБОТЧИК ПРЕРЫВАНИЙ с НИЗКИМ ПРИОРИТЕТОМ
 */
void interrupt low_priority low_isr (void)
{
if (PIR1bits.TMR1IF && PIE1bits.TMR1IE) // Обработали прерывание от TMR1 (Таймер 1)
  {
    PIR1bits.TMR1IF = 0; // Сбросили флаг прерывания
    TMR1 = 25536; // 64M / 8 / (65536 - 25536) = 200Hz
    // Сканируем кнопку
    if ((but_ok == 0) && (!PORTBbits.RB0))
      {
        but_ok = 1;
      }
    if ((but_ok == 2) && (PORTBbits.RB0))
      {
        but_ok = 0;
      }
  } // if (PIR1bits.TMR1IF && PIE1bits.TMR1IE)
} // low_isr END

/*######################################################################################################################
 * ОБРАБОТЧИК ПРЕРЫВАНИЙ с ВЫСОКИМ ПРИОРИТЕТОМ
 */
void interrupt high_isr (void)
{

} // high_isr END

И так — на выходе имеется ШИМ частотой 161 кГц, с отличной линейностью, отсутствие прерываний для формирования ШИМ, да и вообще процессорное время на ШИМ фактически не расходуется. Скриншотов приводить не буду, поскольку имеется видео. Традиционно выложу готовый проект! В проекте все так же, как и в предыдущем примере, с той разницей, что ШИМ формируется модулем CCP.

ШИМ модулем CCP
ШИМ модулем CCP
project_dac_rc_pwm.zip
98.1 KiB
61 Downloads
Детали

Как обычно — вопросы, замечания, предложения в комментариях 🙂

Метки:: , , ,

Ваш отзыв