Главная » Статьи » Уроки по программированию stm32fxxx » Уроки по программированию stm32f1xx

Урок 5: ADC на Std_periph 3.5.0.
Рассмотрев работу с портами ввода вывода, перейдём к использованию этих портов в аналоговом режиме. Т.е. разберёмся с работой аналогово-цифрового преобразователя. Данная статья будет позбита на части и каждую часть я закрою под спойлер. Возможно так будет проще читать статью.
ПРЕДУПРЕЖДЕНИЕ: СТАТЬЯ ПОКА В СТАДИИ ПЕРЕПИСЫВАНИЯ. ПОЛЬЗОВАТЬСЯ ПОКА МОЖНО СТАРОЙ СТАТЬЁЙ ПОД СООТВЕТСТВУЮЩИМ ТЕГОМ.
Примечание: Появилась настройка инжекторных каналов.

Прежде всего рассмотрим параметры АЦП...
Микроконтроллер stm32f1xx имеет на борту 3 12-ти разрядных АЦП. Каждое АЦП может быть подключено к любому из 16-ти аналоговых входов. Более того, каждое из АЦП может сканировать эти входы, снимая с них данные в заданном пользователем порядке.
По окончании преобразования АЦП может выдать прерывание. Вообще АЦП может выдать одно из трёх прерываний: Об окончании преобразования обычного (регулярного) канала, об окончании преобразования по инжекторному каналу и событие по Watchdog. В режиме сканирования прерывание об окончании преобразования выдаётся только по завершении всего сканирования. И при использовании регулярных каналов, в которых данные записываются всегда в один и тот же регистр, вы будете получать результаты только последнего преобразования.
Что бы этого не происходило, в микроконтроллере предусмотрено наличие так называемых инжекторных каналом, имеющих в своём наличии 4 разных регистра для записи данных. Т.е. если вам надо сканировать не более 4-х каналов, то результаты преобразований вы не потеряете. Т.к. каждый канал будет писать данные в свой регистр.
Для параллельного снятия данных сразу по нескольким каналам, предусмотрена возможность одновременного запуска нескольких АЦП. Данный режим получил название Dual Mode.

Подключение АЦП...
Прежде всего рассмотрим подключение АЦП. Для чего нужна каждая ножка показано в таблице 1.

Таблица 1


Из перечисленных ножек интересны -Vоп и +Vоп. Они определяют диапазон напряжений, воспринимаемых АЦП. Если подключить -Vоп к земле, а +Vоп к питанию, то АЦП сможет оцифровать аналоговые сигналы во всём диапазоне от 0, до питания. Т.к. питания МК составляет 3,3В, а разрядность АЦП равна 12-ти, т.е. мы имеем 2^12=4096 уровней квантовая, шум АЦП составит 3,3/4096=0,8 мВ.

Регистры...
Рассмотрим все регистры АЦП.

Задача 1: Регулярный канал...
Теперь решим простую задачу. Нам надо считывать аналоговый сигнал раз в секунду с 10-го канала АЦП.
Т.к. канал один, мы можем использовать регулярный канал, т.к. его настройка несколько проще. Прерывания будут по окончании преобразования, а запуск на Systick.
Настройка производится стандартным способом. Сначала запускаем тактирование АЦП. Потом заполняем структуру, записываем её в регистры и в конце включаем прерывания общие и конкретно по окончании преобразования.

10-й канал находится на ножке С0 поэтому сначала нам надо настроить её. Данная процедура была уже описана в одном из прошлых уроков. Разница только в том, что на этот раз ножку надо настроить как аналоговый вход:

GPIO_InitTypeDef PORT_init_struct;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

PORT_init_struct.GPIO_Pin = GPIO_Pin_0;
PORT_init_struct.GPIO_Speed = GPIO_Speed_10MHz;
PORT_init_struct.GPIO_Mode = GPIO_Mode_AIN;

GPIO_Init(GPIOC, &PORT_init_struct);

В GPIO_Pin написали номер нашей ножки, в GPIO_Speed определили скорость её работы, а в GPIO_Mode инициализируется направление работы порта и выполняемая функция. В данном случае GPIO_Mode_AIN означает, что наш порт будет аналоговым входом.

Теперь приступаем непосредственно к настройке АЦП.
Как вы помните, любая настройка в stm32f начинается с подачи тактового сигнала. И АЦП не исключение.

RCC_ADCCLKConfig(RCC_PCLK2_Div4);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

В первой строке мы определяем делитель тактовой частоты МК. Именно с этого делителя и будет поступать сигнал тактирования на наш МК. В данном случае мы поделили тактовую частоту в 4 раза, т.е. 72/4=18МГц. Делитель поддерживает деление в 2, 4, 6 и 8 раз.
Во сторой строке мы включили тактирование АЦП.

ADC_init_struct.ADC_Mode = ADC_Mode_Independent;//Работаем не в Dual режиме.
ADC_init_struct.ADC_ScanConvMode = DISABLE;//Выключаем сканирование.
ADC_init_struct.ADC_ContinuousConvMode = DISABLE;//Выключаем повторное преобразование по окончании преобразования.
ADC_init_struct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//Выключаем тригеры.
ADC_init_struct.ADC_DataAlign = ADC_DataAlign_Right;//выравнивание полученных данных по правому краю.
ADC_init_struct.ADC_NbrOfChannel = 1;//Число каналов для сканирования.
ADC_Init(ADC1, &ADC_init_struct);//Инициализация.

Коротко прокоментирую.
ADC_Mode - определяем будет ли АЦП работать в спаренном ака Dual режиме или нет. В Dual mode, если верить технической документации на МК, АЦП позволяет запускать сразу 2 канала (1 и 2). При этом результат записывается в регистр для первого АЦП, но в старшие 16 бит. При этом если МК имеет при себе 3-й канал АЦП, то для него сделан отдельный регистр, но работать (судя по всему) параллельно с остальными 2-мя каналами он не может. Тут же возникают проблемы и с ДМА. Дело в том, что в МК не существует канала ДМА для 2-го канала АЦП. техническая документация на этот счёт рекомендует использовать Dual режим и считывать данные с помощью канала ДМА для АЦП1, но из старших 16-ти бит, где и хранятся, как уже было сказано выше, данные для АЦП2. при этом для АЦП3 канал ДМА существует.
Это верно только для stm32f1xx. Для старших серий, таких как stm32f2xx не верно. Там для каждого АЦП существует свой регистр и свой канал. И можно настроить не Dual, а Multy mode, позволяющий запустить одновременно и 3 АЦП.
ADC_ScanConvMode - Этот параметр определяет будет ли АЦП сканировать несколько каналов. Если этот режим включен, то АЦП будет последовательно оцифровывать данные с заданных каналов в заданной последовательности. И каналы и последовательность легко можно задать. Но возникает небольшая проблема со снятием данных. Это всё будет рассмотрено в последней части нашего урока.
ADC_ContinuousConvMode - Этот параметр включает возможность автоматического запуска преобразования сразу по окончании предыдущего преобразования. Т.е. так можно добиться максимальной скорости работы АЦП.
ADC_ExternalTrigConv - пока использовать не приходилось, но судя по всему этот параметр настраивает запуск преобразования по какому либо событию, например переполнению таймера.
ADC_DataAlign - выравнивание данных в 2-хбайтном слове. Есть 2 варианта. ADC_DataAlign_Right при котором данные выравниваются по правому краю, а неиспользуемые биты при этом равны нулю. Т.е. мы получаем обычные числа в 2-х байтах от 0 до 8192. При ADC_DataAlign_Left данные выравниваются по левому краю. Т.е. фактически для 12-ти битного преобразования они увеличиваются в 16 раз. Это может быть использовано например при передаче их через SPI, поддерживающий 12-ти битную передачу данных. Если настроить SPI на передачу начиная со старшего разряда.
ADC_NbrOfChannel - число каналов, которые будет сканировать МК. Сюда записывается требуемое значение, а ниже, если это число больше 1 и ADC_ContinuousConvMode=ENABLE, описывается какие каналы и в какой последовательности они будут сканироваться.

Включаем прерывания по окончании преобразования (EoC - End of Conversion)

ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);

Включаем общие прерывания от АЦП

NVIC_EnableIRQ(ADC1_2_IRQn);

// Включаем АЦП
ADC_Cmd(ADC1, ENABLE);

Делаем калибровку.

// Enable ADC1 reset calibration register
ADC_ResetCalibration(ADC1);
// Check the end of ADC1 reset calibration register
while(ADC_GetResetCalibrationStatus(ADC1));

// Start ADC1 calibration
ADC_StartCalibration(ADC1);
// Check the end of ADC1 calibration
while(ADC_GetCalibrationStatus(ADC1));

Для запуска АЦП используем команду ADC_SoftwareStartConvCmd(ADC1, ENABLE);

АЦП инициирует функцию прерывания void ADC1_2_IRQHandler(void)//ADC interrupt
Для чтения пишем следующее:
ADC1->SR&=~ADC_SR_EOC;//Сбрасываем бит окончания преобразования
DataConv = ADC1->DR;

Задача 2: 3 инжекторных канала...
Усложним задачу. Пусть у нас будут 3 канала - 10,11,14. Остальное остаётся без изменений.
Т.к. у нас 3 канала, нам необходимо настроить сканирование этих самых каналов. Данные опять же будем получать при появлении прерывания об окончании преобразования. Только стоит помнить, что прерывание выдаётся по окончании сканирования. И, если мы и теперь будем использовать регулярные каналы, то регистр DR просто перепишется трижды и мы получим только последнее значение. Нас это не устраивает. Следовательно, нужно использовать инжекторные каналы, в которых каждый канал записывает данные в свой регистр (только помните, что таких может быть не более 4-х).
Тут всё чуть чуть по сложнее. Во-первых надо настраивать инжекторные каналы. Во-вторых, после включения тактирования и общей настройки, необходимо описать в какие регистры какие каналы будут писаться. Только после этого настройка будет окончена.

Первым делом настраиваем ножки. Они, как уже говорилось выше, должны быть настроены как аналоговые входы.
Подаём тактирование на порт С, где и расположены нужные нам выводы.
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

Настраиваем ножки:
GPIO_InitTypeDef PORT_init_struct;

PORT_init_struct.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_4;
PORT_init_struct.GPIO_Speed = GPIO_Speed_10MHz;
PORT_init_struct.GPIO_Mode = GPIO_Mode_AIN;

GPIO_Init(GPIOC, &PORT_init_struct);

Теперь приступаем непосредственно к АЦП:

ADC_InitTypeDef ADC_init_struct;

RCC_ADCCLKConfig(RCC_PCLK2_Div4); //Устанавливаем предделитель тактовой АЦП.
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//Включаем тактирование.

// ADC1 configuration
ADC_init_struct.ADC_Mode = ADC_Mode_Independent;//Не в Dual Mode
ADC_init_struct.ADC_ScanConvMode = ENABLE;//Сканирование включим.
ADC_init_struct.ADC_ContinuousConvMode = DISABLE;//Одиночное преобразование.
ADC_init_struct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//Тригер выключим
ADC_init_struct.ADC_DataAlign = ADC_DataAlign_Right;//Выравнивание по правому краю.
ADC_init_struct.ADC_NbrOfChannel = 3;//Число каналов.
ADC_Init(ADC1, &ADC_init_struct);//Инициализируем.

Теперь, когда общая инициализация прошла успешно, нам надо настроить регистры инжекторных каналов, куда будут писаться наши данные. Ведь именно для этого мы их и используем.

Устанавливаем количество каналов, которые мы собираемся считывать. Программа тогда сразу будет знать сколько вы в неё собираетесь записать каналов. Помните, что нельзя использовать одновременно более 4-х каналов, т.к. у инжекторных каналов только 4 различных регистра.

ADC_InjectedSequencerLengthConfig(ADC1, 3);

//Сопоставляем каналы регистрам. За регистры отвечает 3-й параметр. Параллельно настраиваем порты.
ADC_InjectedChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_71Cycles5);
ADC_InjectedChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_71Cycles5);
ADC_InjectedChannelConfig(ADC1, ADC_Channel_14, 3, ADC_SampleTime_71Cycles5);

//Выключаем тригер.
ADC_ExternalTrigInjectedConvConfig(ADC1, ADC_ExternalTrigInjecConv_None);

// Enable automatic injected conversion start after regular one
ADC_AutoInjectedConvCmd(ADC1, ENABLE);//Это похоже запускает инжекторный канал вместе с регулярным. Т.е. в будущем будем запускать регулярный канал, а снимать с инжекторных. Хотя по поводу данной функции я могу ошибаться.

//Включили прерывания с инжекторного канала по окончании преобразования.
ADC_ITConfig(ADC1, ADC_IT_JEOC, ENABLE);

//Включили прерывания АЦП.
NVIC_EnableIRQ(ADC1_2_IRQn);

//Включили АЦП
ADC_Cmd(ADC1, ENABLE);
/*Теперь надо сделать калибровку. В этом я тоже не разбирался. Так что тут приведена стандартная процедура.*/
// Enable ADC1 reset calibration register
ADC_ResetCalibration(ADC1);
// Check the end of ADC1 reset calibration register
while(ADC_GetResetCalibrationStatus(ADC1));

// Start ADC1 calibration
ADC_StartCalibration(ADC1);
// Check the end of ADC1 calibration
while(ADC_GetCalibrationStatus(ADC1));

На этом заканчивается инициализация.

Для запуска АЦП пишем:
ADC_SoftwareStartConvCmd(ADC1, ENABLE);

Для чтения полученных данных:

void ADC1_2_IRQHandler(void)//ADC interrupt
{
ADC1->SR&=~ADC_SR_JEOC;
DataConvX = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1);
DataConvY = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_2);
DataConvZ = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_3);
ADC_Data_TX = EN;
}

Старая статья...
Будем мигать светодиодом в зависимости от значения напряжения на АЦП. При этом запускаться работа АЦП у нас будет с помощью таймера.
Как и раньше инициализируем порт C9 на выход для светодиода:

//Initialisation PORTC.
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;//Подали такты.
GPIOC->CRH &= ~GPIO_CRH_CNF9;//Сбросили биты в 0.
GPIOC->CRH |= GPIO_CRH_MODE9_0;//режим выхода.

Теперь инициализируем АЦП:

//Initialisation ADC на StdPeriph

//Initialisation ADC на CMSIS
RCC->CFGR &= ~RCC_CFGR_ADCPRE;//
RCC->CFGR |= RCC_CFGR_ADCPRE_DIV2;//
RCC -> APB2ENR |= RCC_APB2ENR_ADC1EN;//Затактировали АЦП
GPIOC -> CRH &= ~GPIO_CRH_CNF10;//Установили аналоговый вход
GPIOC -> CRH &= ~GPIO_CRH_MODE10;//Устанавливаем порт на выход
NVIC_EnableIRQ(ADC1_IRQn);//Включаем прерывания с АЦП.
ADC1->CR2&=~ADC_CR2_CONT; //Сбрасываем бит. Включение одиночных преобразований.
ADC1->SQR1&=~ADC_SQR1_L; //Выбираем преобразование с одного канала. Сканирования нет.
ADC1->SQR3|=(ADC_SQR3_SQ1_1|ADC_SQR3_SQ1_3);//Устанавливаем чтение в первую очередь с 10-го АЦП.
ADC1->CR1|=ADC_CR1_EOCIE;//Включили прерываия при окончании преобразования.

Теперь нам надо включить таймер.

//Initialisation Timer2
RCC -> APB1ENR |= RCC_APB1ENR_TIM2EN;//Тактируем таймер 2
NVIC_EnableIRQ(TIM2_IRQn);// Enable the TIM2 gloabal Interrupt
TIM2 -> DIER |= TIM_DIER_UIE;
TIM2 -> PSC = 0x00005DC0;//24 000 Предделитель т.е. мы полумаем f=1кГц
TIM2 -> ARR = 0x000000E8;
//TIM2 -> ARR = 0x000003E8;//1000 максимальное значение счёта. Т.е. прерывание будет раз в 1 сек.
TIM2 -> CR1 |= TIM_CR1_URS;
TIM2 -> CR1 |= TIM_CR1_CEN;

Описываем обработчик прерывания от таймера:

void TIM2_IRQHandler(void)//TIM2 interrupt
{
   TIM2 -> SR &= ~TIM_SR_UIF;
   ADC1->CR2 |= ADC_CR2_ADON;//Включаем АЦП по таймеру.
}

Теперь описываем обработчик прерывания по завершению работы АЦП:

void ADC1_IRQHandler(void)//ADC interrupt
{
     uint32_t DataConv;

     ADC1->SR&=~ADC_SR_EOC;//Сбрасываем бит окончания преобразования
     DataConv = ADC1->DR;
     if (DataConv > 450)
     {
          GPIOC->BSRR = GPIO_BSRR_BS9;// Выставили бит 3.
     }
     else
     {
          GPIOC->BSRR = GPIO_BSRR_BR9;// Сбросили бит 3.
     }
}

В итоге код будет выглядеть так:

  1. #include "stm32f10x.h"  
  2.   
  3. void InitAll(void)  
  4. {  
  5.    //Initialisation PORTC.  
  6.    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;//Подали такты.  
  7.    GPIOC->CRH &= ~GPIO_CRH_CNF9;//Сбросили биты в 0  
  8.    GPIOC->CRH |= GPIO_CRH_MODE9_0;//режим выхода.  
  9.   
  10.    //Initialization ADC. PortC.0 ADC 10  
  11.    RCC->CFGR &= ~RCC_CFGR_ADCPRE;//  
  12.    RCC->CFGR |= RCC_CFGR_ADCPRE_DIV2;//  
  13.    RCC -> APB2ENR |= RCC_APB2ENR_ADC1EN;//Затактировали АЦП  
  14.    GPIOC -> CRH &= ~GPIO_CRH_CNF10;//Установили аналоговый вход  
  15.    GPIOC -> CRH &= ~GPIO_CRH_MODE10;//Устанавливаем порт на выход  
  16.    NVIC_EnableIRQ(ADC1_IRQn);//Включаем прерывания с АЦП.  
  17.    ADC1->CR2&=~ADC_CR2_CONT; //Сбрасываем бит. Включение одиночных преобразований.  
  18.    ADC1->SQR1&=~ADC_SQR1_L; //Выбираем преобразование с одного канала. Сканирования нет.  
  19.    //Устанавливаем чтение в первую очередь с 10-го АЦП.  
  20.    ADC1->SQR3|=(ADC_SQR3_SQ1_1|ADC_SQR3_SQ1_3);  
  21.    ADC1->CR1|=ADC_CR1_EOCIE;//Включили прерываия при окончании преобразования.  
  22.      
  23.    //Initialisation Timer2  
  24.    RCC -> APB1ENR |= RCC_APB1ENR_TIM2EN;//Тактируем таймер 2  
  25.    NVIC_EnableIRQ(TIM2_IRQn);// Enable the TIM2 gloabal Interrupt    
  26.    TIM2 -> DIER |= TIM_DIER_UIE;  
  27.    TIM2 -> PSC = 0x00005DC0;//24 000 Предделитель т.е. мы полумаем f=1кГц  
  28.    //1000 максимальное значение счёта. Т.е. прерывание будет раз в 1 сек.  
  29.    TIM2 -> ARR = 0x000003E8;  
  30.    TIM2 -> CR1 |= TIM_CR1_URS;  
  31.    TIM2 -> CR1 |= TIM_CR1_CEN;  
  32.    return;  
  33. }  
  34.   
  35.   
  36. void TIM2_IRQHandler(void)//TIM2 interrupt  
  37. {  
  38.     TIM2 -> SR &= ~TIM_SR_UIF;  
  39.     ADC1->CR2 |= ADC_CR2_ADON;//Включаем АЦП по таймеру.  
  40. }  
  41.   
  42. void ADC1_IRQHandler(void)//ADC interrupt  
  43. {  
  44.     uint32_t DataConv;  
  45.   
  46.     ADC1->SR&=~ADC_SR_EOC;//Сбрасываем бит окончания преобразования  
  47.     DataConv = ADC1->DR;  
  48.     if (DataConv > 450)  
  49.     {  
  50.         GPIOC->BSRR = GPIO_BSRR_BS9;// Включили светодиод  
  51.     }  
  52.     else  
  53.     {  
  54.         GPIOC->BSRR = GPIO_BSRR_BR9;// Выключили светодиод  
  55.     }   
  56.   
  57. }  
  58.   
  59. int main(void)  
  60. {  
  61.    InitAll();  
  62.    
  63.    while(1)  
  64.    {  
  65.    }  
  66. }          

Статья не окончена. В ближайшее время я её напишу как следует.

Категория: Уроки по программированию stm32f1xx | Добавил: Korvin (24.07.2012)
Просмотров: 10612 | Рейтинг: 3.8/5
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]