Как получить числовое значение ширины ШИМ-сигнала?
Если Вы зададите этот вопрос гуглу - найдете массу вариантов, советов и реализаций. Так получилось, что ни одна из имеющихся мне не подошла и я сделал свою.
Задача:
Имеется приемник системы радиоуправления. Хотим узнать ширину импульса в микросекундах для канала Х.
Использованное оборудование:
МК - клон Arduino на базе ATmega168(или любой другой клон).
Шилд - Плата расширения для подключения дополнительных датчиков к Arduino. Можете обойтись и без него, пожертвовав удобством.
Система р/у - Турнига 9х или любая другая.
Экран - для контроля значения. Опять же использовал для удобства.
Серва - просто первая подвернувшаяся под руку.
ПО:
Arduino IDE 1.0.2
Библиотека PinChangeInt для удобства работы с прерываниями. Я пробовал и без нее - результат тот же, а с ней нагляднее. Потеря памяти меня устроила.
Библиотека для работы с экраном в архиве с исходниками. Уже не помню где нашел, но остальные память кушали совершенно нищадно и были отложены.
Библиотека RCArduinoFastLib для управления сервой.
Подключение:
1. Питание МК берем с USB.
2. Соединяем экран посредством стандартного шлейфа с колодкой 3х2 через последовательный порт. На шилде разъем помечен как LCD Serial.
3. Приемник соединяем с пятым пином МК при помощи обычного 3х-шнура (как у сервы). Я не долго думая воткнул в первый канал. Опять же удобство шилда - без каких бы то ни было проблем приемник получает питание с МК.
4. На восьмой пин вешаем серву.
Все провода стандартные и ничего паять и обжимать не надо - спасибо шилду.
Алгоритм:
1. Вешаем на входной пин прерывание на изменение уровня.
2. В прерывании проверяем пин. Если состояние 1, то запускаем таймер. Если состояние 0, то останавливаем таймер и вычисляем значение ширины импульса.
3. Вешаем на таймер прерывание на совпадение с некоторым калибровочным значением. Пробовал прерывание по переполнению, но не устроила точность. В перывании увеличиваем на 1 некоторую переменную.
Таймеры 0 и 1 мне нужны для других целей и я решил использовать таймер 2. Но если первые - 16 бит, то этот - 8 бит. Именно поэтому и нужен пункт 3.
Скачать можно тут
Код
#include <PinChangeInt.h>
#include "LCD12864RSPI.h"
#include <RCArduinoFastLib.h>
// Сюда подаем сигнал
#define PWM_PIN 5
// Сюда цепляем серву
#define SERVO_PIN 8
//калибровка таймера
#define TIMER_COMP_VAL 0xfa
volatile uint16_t ovf_cnt;
uint16_t val;
uint16_t val2;
volatile uint16_t temp;
volatile uint16_t temp_cnt;
String temp_str;
char cstr[16];
uint16_t val_prev;
void stop_timer()
{
TCCR2B = 0;
}
void restart_timer()
{
// обнуляем
TCNT2 = 0;
ovf_cnt = 0;
// запускаем
TCCR2B = 0<<CS22 | 1<<CS21 | 1<<CS20;
}
// Прерывание таймера как досчитает до 255
ISR(TIMER2_COMPA_vect) {
// сброс счетчика
TCNT2 = 0;
// увеличение счетчика верхних разрядов
ovf_cnt++;
}
void setup()
{
// Подключаем экран
LCDA.Initialise(); // INIT SCREEN
delay(100);
LCDA.CLEAR();//Clear screen
// Инициализируем библиотеку управления сервой
CRCArduinoFastServos::setFrameSpaceA(3,7*2000);
CRCArduinoFastServos::begin();
// Подключаем серву
CRCArduinoFastServos::attach(0, SERVO_PIN);
// Настройка таймера 2
stop_timer();
val = 0;
val_prev = 0;
TCCR2A = 0;
OCR2A = TIMER_COMP_VAL;
TIMSK2 = (1 << OCIE2A);
//Подключение прерывания по изменению сигнала на входе
PCintPort::attachInterrupt(PWM_PIN, calc, CHANGE);
//Запуск таймера
restart_timer();
}
void loop()
{
val = (temp_cnt*TIMER_COMP_VAL + temp);
val = map(val,0,TIMER_COMP_VAL*4,0,2000);
if (val != val_prev) {
val_prev = val;
// вывод значения на экран
temp_str = String(val_prev, DEC);
temp_str.toCharArray(cstr, temp_str.length()+1);
LCDA.DisplayString(0,0,(unsigned char*)cstr, temp_str.length());//
// установка позиции сервы
CRCArduinoFastServos::writeMicroseconds(0,val_prev);
}
}
void calc()
{
stop_timer();
if(PCintPort::pinState)
{
// начало импульса - стартуем таймер
restart_timer();
}
else
{
// Конец импульса - считаем что получилось
temp = TCNT2;
temp_cnt = ovf_cnt;
}
}
Считаем ширину импульса ШИМ (вариант 2)
Оборудование:
Пульт р/у - Turnigy 9XR
ВЧ-модуль - FrSky DJT
Приемник - FrSky V8FR-II
Микроконтроллер - Arduino Nano v3 втыкаем в шилд для удобства подключения. Скетч подойдет для любого AVR микроконтроллера от 168 до 2560. В принципе влезет и на 48-ю мегу. Но места там больше почти ни на что не останется.
Задача:
Хотим получить значения ширины канальных импульсов для первых шести каналов.
Решение:
AVR микроконтроллеры умеют генерить программные прерывания по изменению состояния входа. Именно этим и стоит воспользоваться. Казалось бы все достаточно просто:
- Вешаем обработчик прерывания на каждый интересующий нас вход.
- Запоминаем время перехода из 0 в 1.
- При переходе из 1 в 0 из текущего времени вычитаем запомненное и так получаем результат.
Вот только есть один подвох - микроконтроллер генерит прерывания не на каждый пин, а в целом на порт.
Для теста была взята 328-я мега. Все ее входы объединены в 3 порта: PORTB - пины 8-13 (+2 используются для подключения внешнего кварцевого резонатора), PORTC пины 14-19 (два последних пина на ардуине не распаяны для совместимости мк в разных корпусах), PORTD - пины 0-7. Мне как-то больше приглянулся PORTD(хотя по сути это не имеет значения).Для каждого порта есть соответствующий бит в регистре PCICR, разрешающий прерывания по изменению уровня на входе. Для порта D это PCIE2. Регистр DDRD задает конфигурацию пинов: 1 в соответствующем разряде задает пин как выход, а 0 - как вход. Так же есть три регистра PCMSK0:2(по одному для каждого порта), которые определяют маску - по изменению состояния каких пинов генерить прерывание.
С учетом всего выше изложенного наш алгоритм придется видоизменить следующим образом:
- Устанавливаем нужные нам пины как входы
- Вешаем обработчик прерывания на интересующий нас порт
- Определяем маску пинов для прерывания
в прерывании:
- Получаем текущее время по таймеру
- Получаем состояние интересующих нас пинов
- Получаем пины, изменившие состояние
- В цикле для каждого пина
- если он изменил состояние
- если переход с 0 на 1, то запоминаем текущее время для канала
- иначе получаем ширину импульса как разность текущего и ранее запомненного времени для канала
Так же стоит учесть, что при переполнении таймера вы получите ошибку. Поэтому стоит добавить контроль на то, что ширина импульса укладывается в разумные пределы. А ошибочные значения таким образом будут игнорироваться.
Собственно ниже код тестового скетча:
void setup()
{
// Все пины порта D кроме нулевого(TX) устанавливаем как входы
DDRD = 1;
// Настраиваем таймер 1 на отсчет 0.5 мкс
TCCR1B = 0;
TCCR1A = 0;
TCNT1 = 0;
TCCR1A = 0;
TCCR1B = 0<<CS12 | 1<<CS11 | 0<<CS10;//0x1A; //start timer with 1/8
// Разрешаем прерывания порта D по изменению уровня
PCICR |= (1 << PCIE2);
// настраиваем маску прерывания - пины 2-7
PCMSK2 = 0xFC;
// открываем UART для вывода результата
Serial.begin(9600);
}
typedef struct {
unsigned long riseTime; // время перехода 0->1
unsigned int lastWidth; // ширина импульса
} tPinTimingData;
volatile static tPinTimingData pinData[6]; //массив каналов
volatile static uint8_t PCintLast;
uint8_t bit;
uint8_t curr;
uint8_t mask;
uint16_t currentTime;
uint16_t time;
// обработчик прерывания порта D
ISR(PCINT2_vect)
{
// текущее время по таймеру 1 в мкс
currentTime = TCNT1>>1;
// Получаем состояние интересующих нас пинов
curr = PIND & 0xFC;
// пины, изменившие состояние
mask = curr ^ PCintLast;
PCintLast = curr;
// в цикле просматриваем каждый пин
for (uint8_t i=0; i < 6; i++) {
// маска текущего пина цыкла
bit = 0x04 << i;
if (bit & mask) {
// если 0->1
if (bit & PCintLast)
// сохраняем текущее время для канала
pinData[i].riseTime = currentTime;
else {
// рассчитываем ширину импульса
time = currentTime - pinData[i].riseTime;
// и сохраняем ее, если таймер не наврал
if ((time >= 800) && (time <= 2200)) {
pinData[i].lastWidth = time;
}
}
}
}
}
void loop()
{
// Вывод результата в UART каждые 0.5 сек.
Serial.println();
for (byte i = 0; i < 6; i++) {
Serial.print("Ch");
Serial.print((int)i + 1);
Serial.print(": ");
Serial.print(pinData[i].lastWidth);
Serial.print(", ");
}
delay(500);
}
По материалам rc-master, автор Зайчиков Александр