Уроки MSP430 LaunchPad. Урок 13: Комбинируем периферию

Периферийные модули MSP430, являются очень полезными инструментами сами по себе. А если мы сможем использовать два, или более периферийных устройств вместе, то перейдём на новый уровень их применения. В этом уроке мы научимся комбинировать модуль компаратора Comparator_A+ и модуль таймера Timer_A, а потом используем их, чтобы построить измеритель ёмкости конденсатора.

Теория


Конденсатор, один из трех фундаментальных пассивных компонентов электроники. Конденсаторы есть практически в любом электронном устройстве вокруг вас. Когда к конденсатору прилагается напряжение, он запасает заряд, пропорциональный приложенному напряжению. Эта пропорция называется – ёмкость. Ёмкость, заряд и напряжение конденсатора связаны формулой q = CU (где q – заряд, C – ёмкость, а U – напряжение). Когда приложенное напряжение убирается, заряд не исчезает моментально, но уменьшается постепенно, так как всегда присутствует какое-то сопротивление между пластинами конденсатора, и есть утечка тока через него.

Способ, которым рассеивается заряд конденсатора легко описать, но это не то, что вы ожидаете услышать, если не знали об этом раньше. Итак, когда исчезает приложенное напряжение U, напряжение на конденсаторе, всё равно U, т.к. заряд еще не исчез. В этот первый момент, маленькая часть заряда вытекает в виде тока через сопротивление между пластинами по широко известному закону Ома: U = IR (где U – напряжение, I – ток, R – сопротивление). После этого, первого момента, сколько заряда уходит во второй момент? Можно подумать, что столько же, сколько и раньше, но это не так. Ведь маленькая часть заряда уже ушла, q теперь меньше, поэтому и напряжение стало немножко меньше. Так как напряжение меньше, то и ток утечки снизился, потом, в следующий момент, еще снизился и т.д. На картинке показана формула разряда конденсатора, понижение напряжения на пластинах идет не линейно, но экспоненциально. Если вы осторожно проследите за изменением напряжения на конденсаторе (с помощью осциллографа или других приборов), вы увидите, что происходит на самом деле.

Итак, изменение не линейно, но его легко описать, поэтому мы можем использовать это для измерения ёмкости конденсатора C. Вкратце, если мы знаем значение сопротивления, все, что нам нужно, это приложить напряжение, и посчитать время, за которое напряжение снизится до другого, известного значения.

Описываем работу измерителя ёмкости


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

Формулу легко найти. Для этого перевернём формулу разряда конденсатора. Теперь видно, что измеряемая ёмкость C, зависит от времени разряда, сопротивления и соотношения между начальным и конечным напряжением на конденсаторе. Мы можем использовать компаратор, чтобы он останавливал таймер, когда напряжение на конденсаторе достигнет выбранного нами опорного напряжения (f). В этом уроке, мы будем разряжать конденсатор до ¼ Vcc, а значит f = 4.

Дальше, я объясню, как настроить периферию и рассмотрю несколько способов, как прочесть полученный результат с MSP430. Вам может быть интересно, какова точность этого измерения. Грубо говоря, это зависит от точности значения сопротивления резистора. Если вы используете 5% резистор, то измерите ёмкость с точностью 5%. Резистор 1% точности, приблизит вас к точности измерения в 1%, но здесь уже начинают оказывать влияние другие факторы.

Взаимодействие компаратора и таймера


Идея, лежащая в измерителе ёмкости проста, но (и об этом говорит, сколько времени я убил, чтобы во всем разобраться) есть некоторые важные детали, требующие решения.

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

Во-вторых, как нам определить, какой порядок величины измеряемой ёмкости? Мелкие конденсаторы разряжаются более быстро, требуя короткого времени измерения (а значит выше процент ошибки измерения). Ёмкие конденсаторы требуют большего времени на заряд, и могут не успеть зарядиться, когда мы уже запустим разряд.

Первый вопрос решается использованием функционала, встроенного в модуль таймера. Второй вопрос, не так просто решаем, но он понятен, мы точно знаем ограничения нашего кода (на максимально измеряемую ёмкость – Прим. пер.). Есть пара вещей, которые могут улучшить ситуацию в целом, но в рамках данного урока, мы не будем этим заниматься.

Новый функционал модуля Timer_A: Вывод


Рассмотрим здесь две новых, для нас, функции модуля таймера. Первая, это возможность использовать таймер для изменения состояния сигнала на выводах микроконтроллера. Вспомним, что таймер имеет несколько регистров захвата/сравнения. (У G221 и G2231, их два). (У G2553 и G2452 их три – Прим. пер.). Каждый из этих регистров, может управлять своим выходом. Мы можем запрограммировать MSP430, изменять выход каждый раз, когда таймер досчитает до значения, хранящегося в регистре. Например, мы можем устанавливать (устанавливать – 1, сбрасывать – 0) выход TA0.1 (где он, смотрим в распиновке микроконтроллера – Прим. пер.), каждый раз, когда таймер досчитает до TACCR1. Или можем переключать состояние выхода TA0.0 каждый раз, когда таймер досчитает до TACCR0. (Это используется в широтно-импульсной модуляции (ШИМ или PWM), мы поговорим об этом в следующих уроках).

Посмотрим на таблицу 12.2 (стр. 316) из руководства по семейству микроконтроллеров x2xx, в ней расписаны все возможные режимы работы таймера на выход.



Новый функционал модуля Timer_A: Захват


Раньше мы говорили только про работу таймера в режиме сравнения. Другой режим, захват, можно использовать для требовательных к временной точности событий. В режиме захвата, таймер записывает значение из регистра счётчика TAR, в регистр захвата/сравнения TACCRx в момент, когда захват происходит. Захват срабатывает на событие, которое может произойти как во внешних линиях, так и во внутренней периферии, включая модуль компаратора. Настроив таймер таким образом, мы сможем считывать значение таймера когда компаратор обнаруживает переход точки сравнения как на возрастающем фронте сигнала (выход компаратора переключается с 0 на 1), так и на спадающем фронте сигнала (выход компаратора переключается с 1 на 0).

Соединяем все вместе


Опишем концепцию реализации измерителя ёмкости. Для точного измерения, нужен точный тактовый сигнал, мы будем использовать откалиброванный сигнал на 1 мГц, от встроенного генератора DCO. Для начала, присоединим резистор к выходу таймера (будем использовать выход TA0.0). Переход, между резистором и конденсатором, присоединен к входу компаратора, другой вывод конденсатора идет на землю. Мы начинаем заряжать конденсатор, установкой вывода TA0.0. И ждем, некоторое заданное время, пока конденсатор зарядится до напряжения питания Vcc. Затем выход сбрасывается (на землю) на время, записанное в TACCR0. Пока напряжение на конденсаторе, превосходит опорное напряжение Vref, на выходе компаратора 1 (предполагается, что мы присоединили RC к неинвертирующему входу, а опорное напряжение подаем на инвертирующий). Когда напряжение на конденсаторе, опускается ниже опорного напряжения, выход компаратора сбрасывается, вызывая захват значения таймера, которое сохраняется в TACCR1. Разница во времени между регистрами TACCR1 и TACCR0, это точное время разряда RC цепи от Vcc до Vref. (Если TACCR0 равняется 0, вычитание не нужно).

Очевидно, что чем длиннее промежуток времени, за который происходит падение напряжения, тем точнее будет измерение. Но что случится, если время превысит 2^16 микросекунд? Регистр счётчика TAR переполнится, и отсчёт пойдет с начала, поэтому в нашей программе, мы должны считать все переполнения счётчика.

Посмотрим на код настройки компаратора, из нашей программы:

void CAinit(void) {
    CACTL1 = CARSEL + CAREF_1;  ; // Опорное напряжение 0.25 Vcc,
                                  // на инвертирующем входе.
    CACTL2 = P2CA4 + CAF;      // Вывод CA1 неинвертирующий вход,
                               // фильтр на выходе.
    CAPD = AIN1;               // Отключаем цифровой блок на P1.7 (технически
                               // этот шаг избыточен).
} // CAinit

Такая настройка компаратора, делает вывод микроконтроллера CA1, неинвертирующим входом компаратора. Если вы посмотрите спецификацию MSP4302211, CA1 находится на выводе P1.1 микроконтроллера. Согласно описанию регистров в руководстве по x2xx, мы настроили CA1 как неинвертирующий вход, установкой P2CA4. (P2CA0 так же контролирует неинвертирующий вход, и для CA1 должен быть обнулён).

Теперь посмотрим на код настройки таймера:

void TAinit(void) {
    TACTL = TASSEL_2 + ID_0 + MC_0; // Используем SMCLK (калиброванные 1 MHz)
                                    // без делителя, таймер остановлен.
    TACCTL0 = OUTMOD_1 + CCIE; // Выход TA0.0 устанавливается по достижении TACCR0
    TACCTL1 = CCIS_1 + SCS + CAP + CCIE; // Используем CAOUT для входа,
                                         // синхронный захват, режим захвата 
                                         // разрешаем прерывания TA1.
                                         // ВНИМАНИЕ: захват пока не работает.
} // TAinit

Таймер настраивается, будучи не запущенным. Установка TACCTL0 в режим OUTMOD_1 устанавливает выход TA0.0, когда счётчик TAR достигает значения TACCR0, где, по умолчанию, стоит 0. Разрешение прерывания таймера, позволит нам следить за переполнением счётчика. В TACCTL1 мы включаем режим захвата, установкой бита CAP. Чтобы понять, как присоединить компаратор, мы должны заглянуть в спецификацию микроконтроллера. Найдите там таблицу “Timer_A2 Signal Connections”, и убедитесь, что используете микроконтроллер с модулем компаратора Comparator_A+. (У G2211 эта таблица на стр. 13, у G2553 на стр. 16, только в нем два таймера Timer0_A3 и Timer1_A3 – Прим. пер.). В колонке озаглавленной “DEVICE INPUT SIGNAL”, найдите “CAOUT (internal)”, и запомните название в следующей колонке “MODULE INPUT NAME”, это CCI1B. Биты CCISx регистра TACCTLx выбирают вход таймера, и в руководстве по x2xx мы можем посмотреть, что эти два бита должны быть установлены в 0b01, для выбора в качестве входа CCIxB. В заголовочном файле нашего микроконтроллера мы можем найти, что такая установка битов возможна мнемоникой CCIS_1.

Установкой бита SCS, мы синхронизируем захват с тактовым сигналом. Мы пока ничего не захватываем, т.к. часы остановлены. Программа настроена на ожидание нажатия пользователем кнопки на P1.3. После нажатия, программа выходит из экономного режима LPM0, и запускает главный код. Происходят такие события, по порядку:

Вначале запускается таймер. Когда таймер пересекает первую отметку, TA0.0 (на выводе P1.5 в этом коде) устанавливается, и начинает заряжать конденсатор. Нам нужно подождать достаточное, для полной зарядки, время. Программа настроена ждать 10 переполнений таймера, что составляет около 655 мс. С этого места включается компаратор, а таймер конфигурируется на сброс TA0.0 на следующем переполнении (т.е. в сумме, мы заряжаем конденсатор в течение 11 переполнений таймера). Мы позволяем таймеру захватить следующее событие, или когда напряжение конденсатора (на P1.1/CA1) упадет до значения опорного Vref, или ¼ Vcc. В этом месте вызывается прерывание. Обработчик прерывания отключает захват и таймер, и возвращается в главный код. В коде происходит возврат к началу бесконечного цикла, и всё повторяется, программа ждёт нажатия кнопки, чтобы начать новое измерение.

До запуска кода программы, обратите внимание, что мы используем вывод P1.1. Что в этом особенного? А то, что необходимо снять перемычку TXD, чтобы программа заработала корректно. Пока мне не подсказали это в комментариях, я убил много времени пытаясь разгадать, в чём проблема!

Листинг программы внизу урока (или оригинал CMeterG2211.c). Что бы всё заработало, вам нужен резистор и конденсатор. Используйте 10 кОм резистор и конденсатор 100 нФ (обозначены как 104 на SMD), если возможно. В отладчике запустите код в свободный полёт и нажмите кнопку. Зеленый светодиод должен переключиться на красный, затем опять на зеленый, сообщая, что измерение закончено. Таймер захватил событие, и сохранил время в TACCR1. К несчастью, пока, у нас нет способа показать это число! Нажмите на паузу в отладчике, и посмотрите регистры таймера. Время записано в TACCR1. Для упомянутой комбинации RC, должно быть число порядка 1400. (Вы можете включить просмотр десятичных значений, нажав правой кнопкой на регистре, и выбрав “decimal” в меню формата.)

Попробуйте увеличить сопротивление резистора до 100 кОм. Вы должны увидеть, что время увеличится в 10 раз. Попробуйте конденсатор 1 мкФ (нужно увеличить максимальное число переполнений, что бы дождаться его зарядки). В окне переменных, мы можем увидеть значение переменной переполнения (overflows). Для того, чтобы это сделать, кликните в этом окошке, и введите имя переменной. С таким ёмким конденсатором, вы должны увидеть, как много раз таймер сделает круг до остановки. Зная измеренное время и сопротивление резистора, можно поставить их в формулу, и посчитать ёмкость конденсатора. Поэкспериментируйте с этой программой. В частности, совпадают ли результаты, при нескольких измерениях в подряд?

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

Упражнение: Если говорить о точности измерения, после экспериментов с программой, что является источником ошибки измерения? Насколько точен тайминг? Что может быть причиной его неточности?

/* CMeterG2211: MSP430G2211 имеет компаратор, который можно использовать
*  для измерения ёмкости конденсатора в RC цепочке,
*  когда он объединён с таймером.
*  Компаратор настроен на опорное напряжение 1/4 Vcc, и останавливает таймер,
*  когда порог пересекается. Значение времени зависит от ёмкости, а точность
*  измерения, главным образом, зависит от точности размера сопротивления,
*  известного нам. Требуются калиброванный источник тактового сигнала, для
*  точного подсчёта времени.
*  
*  Данная версия не включает серийного соединения с компьютером для
*  считывания результата, и изменение света на  светодиодах (P1.0 и P1.6),
*  сигнализирует о завершении измерения. Затем пользователь может
*  нажать паузу в отладчике CCS, и посмотреть результат в регистрах.
*  Резистор присоединён к выводам P1.5 и P1.1, конденсатор к выводам P1.1 и земле.
*  
*  У LaunchPad, перемычка на TXD, должна быть снята, для использования P1.1
*  в этом проекте.  
*/

#include <msp430g2211.h>
 
#define LED1    BIT0
#define LED2    BIT6
#define BTN1   BIT3
#define VCTL    BIT5
#define AIN1    BIT1
 
/*  Глобальные переменные  */
unsigned int overflows;
 
/*  Обявления функций  */
void P1init(void);
void CAinit(void);
void TAinit(void);
 
void main(void) {

    WDTCTL = WDTPW + WDTHOLD;   // Отключаем сторожевой таймер

    BCSCTL1 = CALBC1_1MHZ;
    DCOCTL = CALDCO_1MHZ;       // Такты, - калиброванный 1 мГц

    for(;;) {           // бесконечный цикл
        P1init();
        CAinit();
        TAinit(); 
        _BIS_SR(LPM0_bits + GIE);
        
        overflows = 0;    // Сброс счётчика переполнений
        P1OUT = LED1;     // Гасим LED2, зажигаем LED1 (красный) показать, что идёт измерение.
        TACTL |= MC_2;   // Старт таймера в непрерывном режиме,
                         // выход TA0.0 устанавливается по переполнению.
        while (overflows < 10);
    
        CACTL1 |= CAON;            // Включаем компаратор    
        TACCTL0 = OUTMOD_5 + CCIE; // После следующего переполнения включаем разряд
        TACCTL1 |= CM_2; // Захват из TA1 по спадающему фронту сигнала
        overflows = -1;  // Сброс счётчика переполнений (считаем одно переполнение
                         // и начинаем разряд конденсатора).
        
        _BIS_SR(LPM0_bits + GIE);

    } 	
    
} // main
 
void P1init(void) {
    P1OUT = LED2;    // LED2 (зеленый) зажигается, показывая готовность.
    P1DIR = LED1 + LED2 + VCTL; // Выходы на P1.0 и P1.6 для светодиодов, 
                                // P1.5 для контроля напряжения на RC цепи.
    P1SEL = VCTL; // Назначаем P1.5 на выход от TA0.0, для контроля заряда/разряда

    // Следующие две строчки нужны для инициализации кнопки на LaunchPad 1.5
//    P1REN |= BTN1; 	// Разрешение подтяжки для P1.3
//    P1OUT |= BTN1; 	// Подтяжка P1.3 вверх
    P1IES = BTN1; // Прерывание по спадающему фронту для кнопки с подтяжкой вверх
    P1IFG &= ~BTN1; // Обнуляем флаг прерывания перед разрешением
    P1IE = BTN1;    // Разрешаем прерывание для BTN1
  
} // P1init
 
void CAinit(void) {
    CACTL1 = CARSEL + CAREF_1;  ; // Опорное напряжение 0.25 Vcc,
                                  // на инвертирующем входе.
    CACTL2 = P2CA4 + CAF;      // Вывод CA1 неинвертирующий вход,
                               // фильтр на выходе.
    CAPD = AIN1;               // Отключаем цифровой блок на P1.7 (технически
                               // этот шаг избыточен).
} // CAinit 

void TAinit(void) {
    TACTL = TASSEL_2 + ID_0 + MC_0; // Используем SMCLK (калиброванные 1 MHz)
                                    // без делителя, таймер остановлен.
    TACCTL0 = OUTMOD_1 + CCIE; // Выход TA0.0 устанавливается по достижении TACCR0
    TACCTL1 = CCIS_1 + SCS + CAP + CCIE; // Используем CAOUT для входа,
                                         // синхронный захват, режим захвата 
                                         // разрешаем прерывания TA1.
                                         // ВНИМАНИЕ: захват пока не работает
} // TAinit
 
/*  Обработчики прерываний  */
#pragma vector = PORT1_VECTOR
__interrupt void P1_ISR(void) {
    switch(P1IFG & BIT3) {
        case BIT3:
            P1IFG &= ~BIT3;  // Обнуляем флаг прерывания
            if ((P1OUT & LED1)==LED1)
                return; // Идёт измерение, не реагируем на нажатие кнопки
            else {
                __low_power_mode_off_on_exit(); // Кнопка нажата; продолжаем выполнение программы
                return;
            }
        default:
            P1IFG = 0;  // Обнуляем все случайные флаги прерываний
            return;
    }
} // P1_ISR
    

#pragma vector = TIMERA0_VECTOR
__interrupt void TA0_ISR(void) {
    overflows++;    // Прерывание TA0 возникает, когда Timer_A досчитывает до 2^16,
                    // без прерывания от  Comparator_A
                    // счётчик TAR сбрасывается в 0, добавляем одно переполнение.
} // TA0_ISR

#pragma vector = TIMERA1_VECTOR
__interrupt void TA1_ISR(void) {
    TACCTL1 &= ~(CM_2 + CCIFG); // Останавливаем захват для TA1, обнуляем флаг прерывания.
    TACTL &= ~MC_2;     // Останавливаем Timer_A
    P1OUT = LED2;       // Измерение завершено, включаем зеленый светодиод
    __low_power_mode_off_on_exit();    // Продолжаем выполнение программы
} // TA1_ISR

Архив с программой.

Примечание переводчика: Добавил строчки инициализации кнопки, если у вас LaunchPad 1.5, их необходимо раскомментировать. В IAR всё работает.


Оригиналы статьи на английском: Tutorial 13a: Combining Peripherals и Tutorial 13b: Interfacing the Comparator A+ and Timer A Modules

Предыдущий урок этого цикла: Урок 12: Всё познаётся в сравнении
Следующий урок этого цикла: Урок 14: Текстовый дисплей
  • +9
  • 19 ноября 2012, 01:08
  • Tabke
  • 1
Файлы в топике: CMeterG2211.zip

Комментарии (3)

RSS свернуть / развернуть
Понравилось. Наконец-то появляется материал о практическом применении встроенной периферии, а не о каких-то гипотетических невесть откуда взявшихся данных, подвергаемых сортировке, перетасовке и т.п. Плюсанул бы, но…
+2
  • avatar
  • akl
  • 19 ноября 2012, 15:21
Скажите пожалуйста, зачем эта строчка? Что она делает?
while (overflows < 10);
Это же пустой цикл. или я не прав?
0
Это ожидание 10ти срабатываний прерывания TIMERA0 (в обработчике которого инкрементируется переменная overflows).

В программе допущена ошибка, которая сделает из этого цикла бесконечный, при включении оптимизации, т.к. переменная overflows объявлена без квалификатора volatile. Если быть совсем точным, то не бесконечный, а такой:
if (overflows < 10)
{
    for (;;); /* Бесконечный цикл */
}
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.