ШИМ сигналы играют важную роль во многих современных электронных проектах – начиная от регулировки яркости свечения светодиоды и заканчивая управление серводвигателем. В микроконтроллерах PIC ШИМ сигнал можно сформировать с помощью модуля захвата, сравнения и ШИМ (Compare, Capture and PWM, CCP) при помощи конфигурирования необходимых регистров и этот вопрос ранее мы уже рассматривали на нашем сайте в этой статье. Но у этого метода есть один существенный недостаток.
При этом способе формирование ШИМ сигналов возможно только на контактах RC1 и RC2 микроконтроллера PIC16F877A. Но что делать если нам необходимо большее число контактов с ШИМ сигналами. Например, для управления роботизированной рукой нам необходимо управлять 6 сервомоторами. В этих ситуациях использование модулей CCP будет бесполезным. Поэтому в данной статье мы рассмотрим формирование ШИМ сигналов на обычных контактах ввода/вывода (GPIO pins) микроконтроллера PIC с помощью модулей таймеров. В этом случае мы сможем сформировать так много ШИМ сигналов, сколько нам будет необходимо (разумеется, при ограничениях на число физических контактов микроконтроллера).
Конечно, проблему увеличения числа контактов для формирования ШИМ сигналов можно было бы решить с помощью специальных мультиплексирующих микросхем, но зачем вкладываться в дополнительное аппаратное обеспечение если те же самые задачи можно решить программными средствами? Поэтому в данной статье мы рассмотрим преобразование обычных контактов ввода/вывода (GPIO) микроконтроллера PIC в контакты для формирования ШИМ сигналов (PWM pin). Для тестирования работы нашего проекта мы будем использовать симулятор Proteus с цифровым осциллографом, также с помощью сформированного ШИМ сигнала мы будем управлять положением оси серводвигателя.
Что такое ШИМ сигнал
ШИМ (широтно-импульсная модуляция) – это способ управления аналоговыми сигналами с помощью цифровых значений. Таким образом, можно управлять скоростью вращения двигателей, яркостью свечения светодиода и т.д.
Внешний вид ШИМ сигнала при различных коэффициентах заполнения показан на следующих рисунке.
Важным параметром ШИМ сигнала является его коэффициент заполнения (скважность, в англ. - duty cycle). Коэффициент заполнения ШИМ сигнала представляет собой процент времени, в течение которого сигнал имеет высокий уровень (HIGH, ON). Если ШИМ сигнал всегда находится в состоянии HIGH, то его коэффициент заполнения равен 100%, а если ШИМ сигнал всегда находится в состоянии LOW, то его коэффициент заполнения равен 0%.
Расчет коэффициента заполнения для ШИМ сигнала
Для формирования ШИМ сигнала на обычном контакте ввода/вывода (GPIO) микроконтроллера нам необходимо всего лишь переключать его состояния с заданными длительностями. Но это не так просто как может показаться на первый взгляд. Для каждого цикла ШИМ сигнала длительность состояний включения и выключения должна быть очень точной и для ее определения нельзя использовать обычную функцию задержки. Поэтому для этой цели мы будем использовать модуль таймера и прерывания от таймера. Таким образом, в программе нам необходимо будет задавать коэффициент заполнения/скважность ШИМ сигнала и его частоту. Имена переменных в программе, которые будут использоваться для задания этих значений, приведены в следующей таблице.
Будем предполагать что частоту (frequency) ШИМ сигнала будет задавать пользователь. Тогда период ШИМ сигнала можно будет определить по формуле:
1 |
T_TOTAL = (1/PWM_Frequency) |
Когда пользователь будет изменять значение коэффициента заполнения (Duty cycle) наша программа автоматически должна рассчитывать значения T_ON и T_OFF. Время включения T_ON в этом случае можно определить по формуле:
1 |
T_ON = (Duty_Cycle*T_TOTAL)/100 |
Поскольку период ШИМ сигнала равен сумме времен включения и выключения, то время выключения можно T_OFF можно определить по следующей простой формуле:
1 |
T_OFF = T_TOTAL – T_ON |
Используя эти формулы мы можем приступить к написанию программы для микроконтроллера PIC. В программе будет использовать модуль таймера и модуль АЦП (аналого-цифрового преобразования). Если вы неуверенно чувствуете себя в этих вопросах, то рекомендуем ознакомиться со статьями про использование таймеров и АЦП в микроконтроллерах PIC.
Схема проекта
Схема для формирования ШИМ сигналов на контактах GPIO микроконтроллера PIC представлена на следующем рисунке.
Как видите, схема очень проста, необходимо всего лишь подать питание на микроконтроллер PIC, подключить к нему внешний кварцевый генератор, подключить потенциометр к контакту AN0, а сервомотор – к контакту RD1 (его мы случайным образом выбрали для формирования ШИМ сигнала). И потенциометр, и сервомотор получают питание 5V с регулятора напряжения 7805.
Объяснение программы для микроконтроллера PIC
Полный код программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты.
Первым делом в программе настроим биты конфигурации для микроконтроллера PIC.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// CONFIG #pragma config FOSC = HS // Oscillator Selection bits (HS oscillator) #pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled) #pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled) #pragma config BOREN = ON // Brown-out Reset Enable bit (BOR enabled) #pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming) #pragma config CPD = OFF // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off) #pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control) #pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off) // #pragma config statements should precede project file includes. // Use project enums instead of #define for ON and OFF. #include <xc.h> |
Затем укажем тактовую частоту микроконтроллера – в нашем случае она равна 20 МГц. Далее укажем частоту ШИМ сигнала. Поскольку в нашем проекте мы будем управлять простеньким сервомотором, то в качестве значения данной частоты мы будем использовать 50 Гц (0,05 кГц).
1 2 |
#define _XTAL_FREQ 20000000 #define PWM_Frequency 0.05 // in KHz (50Hz) |
Затем на основе значения частоты ШИМ сигнала рассчитаем его период T_TOTAL по формуле, приведенной выше. Здесь мы его будем делить на 10 чтобы получить значение периода в миллисекундах. В нашем случае значение периода T_TOTAL будет равно 2 миллисекундам.
1 |
int T_TOTAL = (1/PWM_Frequency)/10; //calculate Total Time from frequency (in milli sec)) //2msec |
Затем мы инициализируем АЦП – он нам будет необходим чтобы считывать значение с потенциометра. Более подробно про работу с АЦП в микроконтроллерах PIC вы можете прочитать в этой статье. Также нам будет необходима функция обработки прерывания от таймера, которая будет вызываться каждый раз при переполнении таймера.
Внутри основной функции программы main мы будем настраивать модуль таймера. В нашем проекте мы настроим его так чтобы он переполнялся каждые 0.1ms. Значение для этого, загружаемое в таймер, может быть рассчитано по следующей формуле:
1 |
RegValue = 256-((Delay * Fosc)/(Prescalar*4)) delay in sec and Fosc in hz |
В нашем случае для задержки 0.0001 секунды (0.1ms) с коэффициентом деления предделителя равным 64 и частоты Fosc равной 20 МГц значение нашего регистра TMR0 должно быть равно 248. Таким образом, последовательность команд для конфигурации таймера будет выглядеть следующим образом:
1 2 3 4 5 6 7 |
/*****Port Configuration for Timer ******/ OPTION_REG = 0b00000101; // Timer0 with external freq and 64 as prescalar // Also Enables PULL UPs TMR0=248; // Load the time value for 0.0001s; delayValue can be between 0-256 only TMR0IE=1; //Enable timer interrupt bit in PIE1 register GIE=1; //Enable Global Interrupt PEIE=1; //Enable the Peripheral Interrupt /***********______***********/ |
Затем нам необходимо задать режимы работы используемых контактов. В нашем проекте мы используем контакт AN0 для считывания значения с потенциометра с помощью АЦП и контакты порта PORTD для формирования ШИМ сигналов. Поэтому сконфигурируем их для работы на вывод данных и установим на них уровень low.
1 2 3 4 |
/*****Port Configuration for I/O ******/ TRISD = 0x00; //Instruct the MCU that all pins on PORT D are output PORTD=0x00; //Initialize all pins to 0 /***********______***********/ |
Далее внутри бесконечного цикла while нам необходимо рассчитать значение времени включения T_ON ШИМ сигнала, из которого можно будет определить коэффициент заполнения ШИМ сигнала. Значение времени включения T_ON и коэффициента заполнения ШИМ сигнала в нашем случае будет зависеть от положения ручки потенциометра, поэтому его мы будем непрерывно проверять в цикле. 0.0976 – это значение, на которое нужно умножить число 1024 чтобы получить число 100 – это нам необходимо чтобы рассчитать T_ON. Далее мы умножаем его на 10 чтобы получить результат в миллисекундах.
1 2 3 4 5 6 7 8 9 10 |
while(1) { POT_val = (ADC_Read(0)); //Read the value of POT using ADC Duty_cycle = (POT_val * 0.0976); //Map 0 to 1024 to 0 to 100 T_ON = ((Duty_cycle * T_TOTAL)*10 / 100); //Calculate On Time using formulae unit in milli seconds __delay_ms(100); } |
Поскольку таймер у нас конфигурируется чтобы переполняться каждые 0.1ms, то функция обработки прерывания от него (ISR) также будет вызываться каждые 0.1ms. Внутри данной функции мы будем инкрементировать переменную count каждые 0.1ms. Таким образом мы будем отслеживать прошедшее время. Более подробно о работе с прерываниями в микроконтроллерах PIC вы можете прочитать в этой статье.
1 2 3 4 5 6 |
if(TMR0IF==1) // Timer flag has been triggered due to timer overflow -> set to overflow for every 0.1ms { TMR0 = 248; //Load the timer Value TMR0IF=0; // Clear timer interrupt flag count++; //Count increments for every 0.1ms -> count/10 will give value of count in ms } |
И, наконец, нам необходимо будет переключать состояние контактов GPIO в зависимости от значений времени T_ON и T_OFF. У нас есть переменная count, в которой хранится значение времени в миллисекундах. Поэтому мы будем использовать эту переменную чтобы проверить меньше ли время времени включения T_ON, если меньше, то мы будем удерживать на контакте GPIO состояние high. Если нет, то на контакте GPIO мы будем устанавливать состояние low до тех пор пока не начнется новый период ШИМ сигнала.
1 2 3 4 5 6 7 |
if (count <= (T_ON) ) //If time less than on time RD1=1; //Turn on GPIO else RD1=0; //Else turn off GPIO if (count >= (T_TOTAL*10) ) //Keep it turned off until a new cycle starts count=0; |
Моделирование работы проекта
Для моделирования работы проекта мы использовали симулятор Proteus. Схема нашего проекта, нарисованная в данном симуляторе, представлена на следующем рисунке.
После того как нарисовали схему в симуляторе, загрузите в нее hex файл программы для микроконтроллера и запустите ее на выполнение. После этого на контакте RD1 вы должны увидеть сформированный ШИМ сигнал – значение его коэффициента заполнения будет зависеть от положения ручки потенциометра. На представленной ниже GIF картинке показано как изменяется ШИМ сигнал в зависимости от значения на выходе АЦП, которое, в свою очередь, зависит от положения ручки потенциометра.
Проверка работы проекта с помощью сервомотора
Собранная нами конструкция проекта показана на рисунке ниже. Основой для этой конструкции послужила собранная перфорированная плата из статьи про мигание светодиодом с помощью микроконтроллера PIC.
Загрузите программу в микроконтроллер. После этого вы, вращая ручку потенциометра, сможете управлять положением оси сервомотора. Более подробно работу проекта вы можете посмотреть на видео, приведенном в конце статьи.
Исходный код программы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
/* * File: PIC_GPIO_PWM.c * Author: Aswinth * * Created on 17 October, 2018, 11:59 AM */ // CONFIG #pragma config FOSC = HS // Oscillator Selection bits (HS oscillator) #pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled) #pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled) #pragma config BOREN = ON // Brown-out Reset Enable bit (BOR enabled) #pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming) #pragma config CPD = OFF // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off) #pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control) #pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off) // #pragma config statements should precede project file includes. // Use project enums instead of #define for ON and OFF. #include <xc.h> #define _XTAL_FREQ 20000000 #define PWM_Frequency 0.05 // in KHz (50Hz) //TIMER0 8-bit with 64-bit Prescalar //$$RegValue = 256-((Delay * Fosc)/(Prescalar*4)) delay in sec and Fosc in hz ->Substitute value of Delay for calculating RegValue int POT_val; //переменная для хранения значения с выхода АЦП int count; //timer variable (переменная таймера) int T_TOTAL = (1/PWM_Frequency)/10; // рассчитываем период ШИМ сигнала (в миллисекундах)) //2msec int T_ON=0; // значение времени включения int Duty_cycle; //значение коэффициента заполнения void ADC_Initialize() //подготавливаем модуль АЦП к работе { ADCON0 = 0b01000001; //включаем АЦП и выбираем частоту Fosc/16 ADCON1 = 0b11000000; // используем внутреннее опорное напряжение для работы АЦП } unsigned int ADC_Read(unsigned char channel) //функция для считывания значения с АЦП { ADCON0 &= 0x11000101; //очищаем биты выбора канала АЦП ADCON0 |= channel<<3; //устанавливаем канал 3 __delay_ms(2); //Acquisition time to charge hold capacitor GO_nDONE = 1; //начинаем процесс АЦП while(GO_nDONE); //ждем пока процесс АЦП не завершится return ((ADRESH<<8)+ADRESL); //возвращаем результат } void interrupt timer_isr() { if(TMR0IF==1) // флаг таймера будет устанавливаться каждый раз при его переполнении – у нас это будет происходить каждые 0.1ms { TMR0 = 248; //загружаем значение в таймер TMR0IF=0; // очищаем флаг прерывания от таймера count++; //значение переменной Count инкрементируем каждые 0.1ms -> count/10 будет равно значению count в миллисекундах (ms) } if (count <= (T_ON) ) RD1=1; else RD1=0; if (count >= (T_TOTAL*10) ) count=0; } void main() { /*****Port Configuration for Timer ******/ OPTION_REG = 0b00000101; // Timer0 с внешней частотой и коэффициентом деления предделителя равным 64 // также используем внутренние подтягивающие резисторы TMR0=248; // загружаем значение для времени переполнения 0.0001s; delayValue может быть только в диапазоне 0-256 TMR0IE=1; //разрешаем прерывания от таймера в регистре PIE1 GIE=1; // глобальное разрешение прерываний PEIE=1; // разрешение прерываний от периферийных устройств /***********______***********/ /*****Port Configuration for I/O ******/ TRISD = 0x00; //Instruct the MCU that all pins on PORT D are output (контакты PORT D будут работать на вывод данных) PORTD=0x00; //инициализируем все контакты в значение 0 /***********______***********/ ADC_Initialize(); while(1) { POT_val = (ADC_Read(0)); //считываем значение с потенциометра используя АЦП Duty_cycle = (POT_val * 0.0976); //преобразуем из диапазона 0-1024 в диапазон 0-100 T_ON = ((Duty_cycle * T_TOTAL)*10 / 100); //рассчитываем время включения ШИМ сигнала в миллисекундах __delay_ms(100); } } |
Представленный ниже код не компилируется....
Ну а какую ошибку выдает? Если в код программы закралась опечатка (к сожалению, такое бывает), то давайте попробуем ее найти