LPCXpresso Урок 7. АЦП. Измеряем напряжение.
Единичка-нолик это конечно хорошо, но иногда надо иметь дело с более «многозначными» величинами. Давайте в рамках курса для новичков запустим на контроллере АПЦ.
Будем использовать самый простой режим: измерение по запросу. Прерывания от АЦП в этом случае нам не нужны, что позволит значительно упростить код.
Схема подключения
К предыдущему уроку (с подключенной кнопкой) добавим следующую схему:
Потенциометр (регулируемый резистор) не обязательно брать на 47кОм, можно выбрать любой имеющийся у вас с сопротивлением от 1кОм до 100кОм.
Дорабатываем библиотеку
К сожалению, в библиотеке LPC13xx_Lib функции для работы с АЦП так же отсутствуют. Что ж, добавим и их.
В проекте LPC13xx_Lib на папке inc вызываем контекстное меню и выбираем пункт New..., а в нем Header File:
В поле Header File вводим adc.h и жмем Finish. Среда сгенерирует нам шаблон файла, нам остается добавить в него немного текста, строки с ADC_H_ которого позволяют подключить файл только один раз, во избежание разных недоразумений.
Нам осталось добавить пару строк и получить файл на подобие:
#ifndef ADC_H_
#define ADC_H_
#define ADC_NUM 8 /* у LPC13xx 8 каналов */
extern void ADCInit( uint32_t ADC_Clk );
extern uint32_t ADCRead( uint8_t channelNum );
#endif /* ADC_H_ */
Константа ADC_NUM определяет количество каналов АЦП доступных в контроллере. Далее идет описание двух функций, которые мы с вами добавим.
Приступим к реализации описанных функций. Действуем по аналогии с добавлением заголовочного файла. В проекте LPC13xx_Lib на папке src вызываем контекстное меню и выбираем пункт New... а в нем Source File. В поле Source File вводим adc.с и жмем Finish. Среда снова сгенерирует нам шаблон, но уже для файла реализации.
Приступим к заполнению. В начале файла реализации подключим необходимые заголовочные файлы и опишем константу.
#include "LPC13xx.h" /* LPC13xx Peripheral Registers */
#include "adc.h"
#define ADC_DONE 0x80000000
Константа ADC_DONE (подсмотрена в UM10375) содержит битовую маску для обнаружения завершения преобразования. Когда данный бит в регистре канала АЦП будет единичным - преобразование завершено и результат можно считать действительным.
Теперь нам требуется реализовать функцию настройки АЦП:
void ADCInit( uint32_t ADC_Clk )
{
// Включаем питание на АЦП (отключаем режим Power down)
LPC_SYSCON->PDRUNCFG &= ~(0x1<<4);
// Включаем тактирование на АЦП
LPC_SYSCON->SYSAHBCLKCTRL |= (1<<13);
LPC_IOCON->JTAG_TDI_PIO0_11 = 0x02; // Выбор функции AD0 для пина
LPC_IOCON->JTAG_TMS_PIO1_0 = 0x02; // Выбор функции AD1 для пина
LPC_IOCON->JTAG_TDO_PIO1_1 = 0x02; // Выбор функции AD2 для пина
LPC_IOCON->JTAG_nTRST_PIO1_2 = 0x02; // Выбор функции AD3 для пина
// LPC_IOCON->ARM_SWDIO_PIO1_3 = 0x02; // Выбор функции AD4 для пина (используется отладчиком)
LPC_IOCON->PIO1_4 = 0x01; // Выбор функции AD5 для пина
LPC_IOCON->PIO1_10 = 0x01; // Выбор функции AD6 для пина
LPC_IOCON->PIO1_11 = 0x01; // Выбор функции AD7 для пина
LPC_ADC->CR = ((SystemCoreClock/LPC_SYSCON->SYSAHBCLKDIV)/ADC_Clk-1)<<8;
}
Все выводы каналов АЦП настраиваем на функцию "вход АЦП". Все кроме ARM_SWDIO_PIO1_3 (AD4). Дело в том, что этот вывод используется на LPCXpresso для отладчика, и если вы назначите ему функцию "вход АЦП" то вы попросту не сможете производить внутрисхемную отладку. По данной причине строка упомянутого вывода закомментирована.
Ну и последняя строка в функции устанавливает в регистр CR делитель частоты для модуля АЦП. Иными словами задает частоту тактирования модуля АЦП. Значение рассчитаем исходя из переданной в качестве параметра в функцию желаемой частоты (по даташиту рекомендуется значение не больше 4.5МГц). Это не есть частота преобразования, преобразование займет, в нашем случае, 11 тактов этой частоты.
Ну и, наконец, функция получения значения АЦП:
uint32_t ADCRead( uint8_t channelNum )
{
uint32_t regVal, ADC_Data;
if( channelNum >= ADC_NUM ) { // номер канала должен быть от 0 до 7. если он больше
return 0; // то сразу возвращаем 0
}
LPC_ADC->CR &= 0xFFFFFF00; // сбрасываем выбор канала
LPC_ADC->CR |= (1 << 24) | (1 << channelNum); // Переключаем канал, запуск преобразования
while( 1 ) { // ожидаем завершения преобразования
regVal = ((volatile unsigned long *)&LPC_ADC->DR0)[channelNum];
if ( regVal & ADC_DONE ) { // читаем результат преобразования и проверяем на завершенность
break;
}
}
LPC_ADC->CR &= 0xF8FFFFFF; // останавливаем АЦП
ADC_Data = ( regVal >> 6 ) & 0x3FF; // извлекаем результат
return ADC_Data; // Возвращаем результат преобразования
}
Логика проста и, по-моему, ясна из комментариев. Объясню только цикл получения результата преобразования. Мы получаем адрес регистра первого канала АЦП (&LPC_ADC->DR0) и приводим его к типу указателя на "изменяемое извне" (volatile unsigned long *). Это заставляет компилятор не оптимизировать код и всегда читать значение из регистра, и не пытатася его кэшировать. Это требуется, что бы мы ни ушли в бесконечный цикл при "ожидании изменения кэшированного/неизменяемого значения". После этого пользуемся тем, что регистры каналов расположены последовательно в памяти и просто извлекаем данные из требуемого нам канала ([channelNum]). Ну а далее, если в извлеченном значении отсутствует бит готовности данных (ADC_DONE), то повторяем чтение. Иначе полученный результат является действительным, и мы завершаем цикл чтения.
Дорабатываем программу
Для вывода результатов преобразования мы воспользуемся отладочной консолью. По этому в настройках проекта указываем использовать библиотеку Redlib (semihosting).
В начале файла main.c добавляем следующие строки:
#include <stdio.h>
#define ADC_CLK 4500000 /* для частоты преобразования 4.5МГц */
#define ADC_CHAH 5 /* Используемый канал АЦП */
#define ADC_VOLTAGE 3150 /* Напряжение опоры АЦП в миливольтах */
Подключение stdio.h нужно было для использования функции printf.
Далее определили константу для частоты модуля АЦП. Константа ADC_CHAH определяет номер канала, к которому вы подключили резистор (в нашей схеме это пятый, но можете выбрать и другой). Константа ADC_VOLTAGE определяет напряжение опоры и нужна для вывода в консоль измеренного АЦП значения напряжения как величины милливольт, а не просто абстрактной величины. Напряжение опоры для нашего контроллера совпадает с напряжением питания, которое составляет 3.3В, но у меня оно на плате составляло только 3.15В.
Правим функцию main до приобретения следующего вида:
int main(void) {
uint32_t adcVal = 0, mV = 0;
GPIOInit();
GPIOSetDir(LED_PORT, LED_BIT, 1);
GPIOSetValue(LED_PORT, LED_BIT, LED_OFF);
GPIOSetDir(BUTTON_PORT, BUTTON_BIT, 0);
SysTick_Config(SystemCoreClock / 1000); // настройка таймера на период 1мс
ADCInit(ADC_CLK);
while(1) {
adcVal = ADCRead(ADC_CHAH);
if(GPIOGetValue(BUTTON_PORT, BUTTON_BIT) == BUTTON_DOWN) {
mV = adcVal * ADC_VOLTAGE / 1023; // Преобразуем значение АЦП в миливольты
printf("ADC %d value = %4d , volage = %d mV\n", ADC_CHAH, adcVal, mV);
delay_ms(500);
} else {
GPIOSetValue(LED_PORT, LED_BIT, LED_ON);
delay_ms(adcVal);
GPIOSetValue(LED_PORT, LED_BIT, LED_OFF);
delay_ms(adcVal);
}
}
return 0 ;
}
В общем-то, уже знакомая нам инициализация, только добавился вызов ADCInit(ADC_CLK) инициализирующий АЦП. Далее идет бесконечный цикл.
Вызовом ADCRead получаем значение запрошенного канала АЦП. После этого в зависимости от текущего состояния кнопки мы выполняем один из двух алгоритмов.
Если кнопка нажата, то:
- преобразуем считанное из АЦП значение в напряжение;
- выводим в консоль отладчика номер канала, измеренную величину и рассчитанное напряжение в милливольтах
- ждем пол секунды для разграничения измерений.
- зажигаем светодиод;
- ждем в течение времени пропорционального измеренной величине
- гасим светодиод
- повторно ждем в течение времени пропорционального измеренной величине
Зачем нам две ветки? А за тем, что бы можно было запускать пример без отладчика. Если кнопка не будет нажата, то вызова printf не будет, а, следовательно, контроллер не зависнет при выполнении без отладчика. А если мы запустили пример под отладчиком, то просто нажимаем кнопку и в консоли у нас появятся вполне читабельные сообщения, которые гораздо информативнее мигающего светодиода.
Запуск
Собственно запускаете приложение и наблюдаете уже описанные явления. Измените положение потенциометра и наблюдайте за изменением поведения светодиода, либо за сообщениями в консоли.
Статистика
Данный проект в Debug версии с semihosting библиотекой занял 18940 байт. Если собрать его с библиотекой Redlib (none) и удалить вызов printf получится ~3764 байт. Разница в 5 раз. Но скажите что лучше: видеть читаемое значение на экране монитора или пытаться определить его по миганию светодиода? Я думаю 15кб кода не большая цена за читабельность.
Вместо заключения
Хочу отметить точность измеренного напряжения. Дело в том, что питание у нас не является достаточно чистым, опора у нас объединена с питанием непосредственно, да к тому же АЦП имеет свою ошибку измерений (о которой можете прочитать в документации к контроллеру). По этой простой причине полученные с АЦП значения могут и не совпадать с величинами, измеренными скажем мультиметром (который так же измеряет с ошибкой). Для того, что бы величины совпадали (на сколько это возможно), надо: во-первых, качественное питание; во-вторых, производить калибровку, которую мы не выполняли. Но, в общем-то, АЦП можно пользоваться.
Файлы: blinky_adc.zip