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 получаем значение запрошенного канала АЦП. После этого в зависимости от текущего состояния кнопки мы выполняем один из двух алгоритмов.
Если кнопка нажата, то:

  1. преобразуем считанное из АЦП значение в напряжение;
  2. выводим в консоль отладчика номер канала, измеренную величину и рассчитанное напряжение в милливольтах
  3. ждем пол секунды для разграничения измерений.
Если же кнопка отжата, то:
  1. зажигаем светодиод;
  2. ждем в течение времени пропорционального измеренной величине
  3. гасим светодиод
  4. повторно ждем в течение времени пропорционального измеренной величине
Тем самым скорость мигания светодиода будет тем выше, чем меньше измеренное значение.

Зачем нам две ветки? А за тем, что бы можно было запускать пример без отладчика. Если кнопка не будет нажата, то вызова printf не будет, а, следовательно, контроллер не зависнет при выполнении без отладчика. А если мы запустили пример под отладчиком, то просто нажимаем кнопку и в консоли у нас появятся вполне читабельные сообщения, которые гораздо информативнее мигающего светодиода.

Запуск

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

Статистика

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

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

Хочу отметить точность измеренного напряжения. Дело в том, что питание у нас не является достаточно чистым, опора у нас объединена с питанием непосредственно, да к тому же АЦП имеет свою ошибку измерений (о которой можете прочитать в документации к контроллеру). По этой простой причине полученные с АЦП значения могут и не совпадать с величинами, измеренными скажем мультиметром (который так же измеряет с ошибкой). Для того, что бы величины совпадали (на сколько это возможно), надо: во-первых, качественное питание; во-вторых, производить калибровку, которую мы не выполняли. Но, в общем-то, АЦП можно пользоваться.

Файлы: blinky_adc.zip

Hosted by uCoz