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.c и жмем 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 получаем значение запрошенного канала АЦП. После этого в зависимости от текущего состояния кнопки мы выполняем один из двух алгоритмов.
Если кнопка нажата, то:
  1. преобразуем считанное из АЦП значение в напряжение;
  2. выводим в консоль отладчика номер канала, измеренную величину и рассчитанное напряжение в милливольтах
  3. ждем пол секунды для разграничения измерений.
Если же кнопка отжата, то:
  1. зажигаем светодиод;
  2. ждем в течение времени пропорционального измеренной величине
  3. гасим светодиод
  4. повторно ждем в течение времени пропорционального измеренной величине
Тем самым скорость мигания светодиода будет тем выше, чем меньше измеренное значение.
Зачем нам две ветки? А за тем, что бы можно было запускать пример без отладчика. Если кнопка не будет нажата, то вызова printf не будет, а, следовательно, контроллер не зависнет при выполнении без отладчика. А если мы запустили пример под отладчиком, то просто нажимаем кнопку и в консоли у нас появятся вполне читабельные сообщения, которые гораздо информативнее мигающего светодиода.

Запуск

Собственно запускаете приложение и наблюдаете уже описанные явления. Измените положение потенциометра и наблюдайте за изменением поведения светодиода, либо за сообщениями в консоли.

Статистика

Данный проект в Debug версии с semihosting библиотекой занял 18940 байт. Если собрать его с библиотекой Redlib (none) и удалить вызов printf получится ~3764 байт. Разница в 5 раз. Но скажите что лучше: видеть читаемое значение на экране монитора или пытаться определить его по миганию светодиода? Я думаю 15кб кода не большая цена за читабельность.

Вместо заключения

Хочу отметить точность измеренного напряжения. Дело в том, что питание у нас не является достаточно чистым, опора у нас объединена с питанием непосредственно, да к тому же АЦП имеет свою ошибку измерений (о которой можете прочитать в документации к контроллеру). По этой простой причине полученные с АЦП значения могут и не совпадать с величинами, измеренными скажем мультиметром (который так же измеряет с ошибкой). Для того, что бы величины совпадали (на сколько это возможно), надо: во-первых, качественное питание; во-вторых, производить калибровку, которую мы не выполняли. Но, в общем-то, АЦП можно пользоваться.
  • +3
  • 14 сентября 2011, 12:29
  • angel5a
  • 1
Файлы в топике: blinky_adc.zip

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

RSS свернуть / развернуть
tl;dr
Какое время выборки сигнала (sampling time) установлено для АЦП?
При большом сопротивлении подстроечника и маленьком времени выборки АЦП будет врать.
0
Господа, нужна помощь…
Случилась неприятность… вместо
«LPC_SYSCON->SYSAHBCLKCTRL |= (1<<13);»
умудрился написать
«LPC_SYSCON->SYSAHBCLKCTRL = (1<<13);»,
в результате не могу подключиться к плате:
0
пишет: «15: Target error for commit flash write: Et: Flash driver is not ready.»
Видимо это связано с затиранием битов 1-4 в регистре SYSAHBCLKCTRL.
Подскажите, как можно очистить/перезаписать flash???
0
Посадите вывод ресет на землю и залейте исправленную прошивку.

Либо, соберите схему для USB-bootloader'а из Урок 9. USB-bootloader. Прошиваем контроллер подручными средствами. и удалите файл прошивки с контроллера, после этого льете новую исправленную.
0
учусь программировать на МК 3 дня. Начиная с урока №1. -)

Застрял на данном уроке… и наконец-то нашел причину.
на LPC1114 компилятор ругается на

regVal = ((volatile unsigned long *)&LPC_ADC->DR0)[channelNum];

ругается на DR0, посмотрев в LXP11xx.h есть строчка:
__IO uint32_t DR[8]; 


Изменив DR0 на DR[0] все заработало.

Огромное спасибо за уроки -)!

з.ы. правда до сих пор не разобрался че ругается на:
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 для пина


..\src\adc.c:21:12: error: 'LPC_IOCON_TypeDef' has no member named 'JTAG_TDI_PIO0_11'
..\src\adc.c:22:12: error: 'LPC_IOCON_TypeDef' has no member named 'JTAG_TMS_PIO1_0'
..\src\adc.c:23:12: error: 'LPC_IOCON_TypeDef' has no member named 'JTAG_TDO_PIO1_1'
..\src\adc.c:24:12: error: 'LPC_IOCON_TypeDef' has no member named 'JTAG_nTRST_PIO1_2'


ну пока-что закомментировал.
0
Изменив DR0 на DR[0] все заработало.
В моей версии библиотеки регистры данных были описаны отдельными записями в структуре по этому DR0, у вас же они более культурно объеденены в массив, так что действия верные, но лучше что бы строка выглядила в итоге так:
regVal = ((volatile unsigned long *)&LPC_ADC->DR[channelNum];

з.ы. правда до сих пор не разобрался че ругается на:
Дело в том что в LPC1343 несколько каналов АЦП разделяют выводы JTAG интерфейса. Этим выводам дано спецефическое еми (JTAG_...) что бы при написании кода было видно что вывод то не обычный. В LPC1114 JTAG либо на других выводах, либо в библиотеке даны другие имена.
ну пока-что закомментировал.
Вообще-то не правильно использовать LPC13xx_Lib с контроллером LPC1114, для него надо взять библиотеку LPC11xx_Lib.
0
В поле Source File вводим adc.с и жмем Finish
 В имени файла русская «с» вместо латинской, если скопипастить название, среда не сожрет такой файл, обратите внимание…
0
Поменял обе с, будем надеятся пофиксил.
0
В свое время бодяжил псевдо-АЦП на меге 8515 (нужна была параллельная память, ацп и минимальная стоимость). Так вот у этого кристала напрочь отсутствует ацп, но есть компаратор. Компаратор + таймер + плюс конденсатор позволили подключить и измерять угол поворота потенциометра. Что -то вроде такого: Включаешь ногу по правому флангу, запускаешь таймер. по центру стоит кондерчик на массу. Как только уровень заряда конденсатора привысил внутреннее напряжение стабилизатора ( он есть на кристале) — срабатывает компаратор — и таймер запоминает время срабатывания. Следующий этап — разрядка кондерчика. Потом повторяем все тоже самое по левому флангу. В итоге получаем два значения таймера. Угол поворота вычисляется как L/(L+R). Вуаля. Помучился немного с прогой, зато несколько рублей съекономил на массовую штучку. И не надо говорить про STM32. Их в то время еще в проекте не было))
0
Можно еще было сигма-дельта АЦП сделать, не сложнее. AlexX1810 об этом писал. Хотя для описанной тобой задачи — опрос потенциометра — твой метод практически общепринят. Так делается, например, в некоторых джостиках Logitech, где применен USB-MCU без АЦП (CY7C6800x, кстати, один из аппнотов к этим МК как рз такой джостик и описывает), нередко применяется в китайских джоях и пультах от РУ игрушек.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.