STM32F0. 3-фазный PWM. Настройка TIM1, TIM2, TIM14, DMA, ADC и обработка их прерываний.

Опыт накоплен в процессе изучения STM32F0DISCOVERY на примере всем понятной задачи управления асинхронным трехфазным двигателем.

Не смотря на использование сторожевого таймера ADC по току при  работе с питанием 220, я настоятельно рекомендую аппаратную защиту по току земли с отключением драйверов ключей.

В проекте пока не рассмотрены варианты:

  1. Плавный пуск. Видится стабилизацией тока подключением файлов синуса с разной максимальной амплитудой
  2. Стабилизация тока при анализе угла Фи. Зарезервирован TIM3
  3. Быстрый реверс инерционного двигателя. При переходе через ноль напряжение на ключах возрастет до двух раз, дабы мотор будет работать как генератор
  4. Механизмы гашения для быстрого останова двигателя

Рис. 1. Использование счетчиков для модуляции синуса

В отличие от STM32F4, STM32F0 не имеет аппаратной поддержки операций с плавающей точкой, поэтому синус считаем в MsExcel. Стараемся работать с круглыми числами, такими как 1024, 512 & e.t. И не забывать, что логический сдвиг вправо – это деление пополам.

Файл MsExcel для расчета значений синуса

Рис. 2. Прерывания счетчиков на картинке ШИМ

Разгон двигателя осуществляется поворотом энкодера, либо кнопками. На монитор выводятся элементы массива из файла F_L_Disp.h от 100 оборотов в минуту до 10000 с шагом 100 оборотов. В счетчик грузятся соответствующие элементы массива Fr_Loc_Tim2 [101] из файла F_L_Tim2.h

Между шагами реализован плавный разгон двигателя с шагом приращения счетчика 121. Это разница значений счетчика в частотах вращения 9900 и 10000 об/мин. Линейность разгона гарантируется изменением приращения счетчика Mod (N/121) + Div (N/121), где N разность между соседними элементами Fr_Loc_Tim2 [n-(n+1)] (в файле F_L_Tim2.h).

Mod_Fr_Loc_Tim2 [100] и Div_Fr_Loc_Tim2 [100] посчитаны заранее и грузятся из Mod_F_L_Tim2.h и Div_F_L_Tim2.h

Скорость разгона регулируется от -10 до +10, где 0 одна секунда выполнения шага разгона

Значения ускорения счетчика разгона содержатся в массиве Fr_Ass_Tim14 [21] из файла F_Acc_Tim14.h

Файл MsExcel для расчета целой и дробной частей приращения счетного регистра Tim2

Файл MsExcel для расчета значений счетного регистра Tim14 (Скорость изменения частоты)

Массив значений синуса фазы A

Массив значений синуса фазы B

Массив значений синуса фазы C

Массив значений индикации дисплея. От 100 до 10000 оборотов в минуту с шагом 100. F_L_Disp.h

Массив значений счетного регистра Tim2, соответствующих дисплею. F_L_Tim2.h

Массив значений целой части приращения счетного регистра TIM2. Mod_F_L_Tim2.h

Массив значений остатка приращения счетного регистра TIM2. Div_F_L_Tim2.h

Массив значений счетного регистра TIM14. (Скорость разгона двигателя). F_Acc_Tim14.h

Вовсе не обязательно обновлять значения компараторов TIM1 с частотой несущей ШИМ после загрузки очередных значения (точки синуса)

TIM1->CR1 |= TIM_CR1_UDIS;      // Запретить обновление

TIM1->DIER &= ~TIM_DIER_UIE;    // Запретить прерывание по переполнению

Разрешать мы его будем в TIM2 после загрузки очередных значений ШИМ

Рис 3. Запрещение и разрешение прерывания TIM1

Аналогично, нет никакого смысла постоянно измерять значения токов и напряжения. Для этого будем использовать компаратор TIM2->CCR3, TIM2->DIER |= TIM_DIER_CC3IE, который по совпадению вызывает прерывание, в котором разрешаем прерывание АЦП по концу последовательности (позиционируем DMA1 на первый датчик).

Прерывание АЦП разрешает прерывание DMA1 по концу буфера. Выставляем флаг и обрабатываем буфер в основном цикле. TIM2 загружает новые значения синусов (24 точки) по переполнению минимум через 12000 тиков.

Оставим, скажем 2000 тиков на переходные процессы и запустим АЦП на цикл измерений с усреднением.

АЦП работает от внутреннего генератора 14 МГц. Опрашиваем регулярную группу из 4 каналов. Ограничим время выборки АЦП 55,5 тиков RCC_CR2_HSI14ON. Для группы будет 222 тика АЦП или 761 тик процессора (таймера). При частоте синуса 166,(6) Гц и 24 точках мы можем себе позволить всего 8 значений для усреднения. (6089 тиков счетчика) или успеть снять одно значение датчиков при времени выборки 239,5 тиков. (3285 тиков процессора).

Арифметика получается не веселой даже без учета времени обработки прерываний.

Частота ШИМ определяется количеством повторений значений синуса.

Файл MsExcel использования контактов STM32F0DISCOVERY

Фрагмент кода определения переменных, используемых в примере

//------------------------------------------------------------------------------

//Обработчики прерываний таймеров ADC & DMA

//------------------------------------------------------------------------------

void  TIM1_BRK_UP_TRG_COM_IRQHandler (void)         // Прерывание при сбое, обновлении, пуске и коммутации таймера TIM1

{

if (TIM1->SR & TIM_SR_UIF)   {

TIM1->SR &= ~TIM_SR_UIF;     // Сбрасываем флаг прерывания по переполнению

TIM1->CCR1 = *p_Sin_A;

TIM1->CCR2 = *p_Sin_B;

TIM1->CCR3 = *p_Sin_C;

TIM1->CR1 |= TIM_CR1_UDIS;      // Запретить обновление

TIM1->DIER &= ~TIM_DIER_UIE;    // Запретить прерывание по переполнению

// Разрешать мы его будем в TIM2 после загрузки очередных значений ШИМ

}

//-----

if (TIM1->SR & TIM_SR_BIF)   {

Driver_Off      // Запретить работу ключей драйверов полевиков "оборвать провод"

TIM1->SR &= ~TIM_SR_BIF;    // Сбрасываем флаг аварийного останова, причем он не сбросится, пока PB12 != 0.

Mod_F = 0;      // Программный стоп

Act_F = 0;      // до обработки включения

Led_BIF_On

}

}

//------------------------------------------------------------------------------

void  TIM2_IRQHandler (void)    // Прерывание от таймера TIM2

{

if (TIM2->SR & TIM_SR_UIF)   {

TIM2->SR &= ~TIM_SR_UIF;    // Сбрасываем флаг прерывания по переполнению

if (Sin_counter-- > 0) {

*(p_Sin_A++);

*(p_Sin_B++);

*(p_Sin_C++);

TIM1->DIER |= TIM_DIER_UIE;      // Разрешить прерывание TIM1 по переполнению

TIM1->CR1 &= ~TIM_CR1_UDIS;      // Разрешить обновление

}

else { if (Fl_Rev == 0) {

p_Sin_A = &Sin_A [0];

p_Sin_B = &Sin_B [0];

p_Sin_C = &Sin_C [0];

}

else {

p_Sin_A = &Sin_B [0];      // Реверс. Поменять фазы местами и крутить в другую сторону

p_Sin_B = &Sin_A [0];

p_Sin_C = &Sin_C [0];

}

TIM1->DIER |= TIM_DIER_UIE;      // Разрешить прерывание по переполнению

TIM1->CR1 &= ~TIM_CR1_UDIS;      // Разрешить обновление

Sin_counter = Sin_abcissa;

Period_Fl = 1;

}

TIM2->CCR3 = Tim2_Start_AMA_ADC;    // Грузим в компаратор значение включения АЦП

}

//-----

if (TIM2->SR & TIM_SR_CC3IF)   {       // прерывание по совпадению компаратора TIM2->CCR3

TIM2->SR &= ~TIM_SR_CC3IF;          // Сбрасываем флаг прерывания

//!!!!!

ADC1->CR |= ADC_CR_ADSTP;               // ADC stop of conversion command

while (ADC1->CR & ADC_CR_ADSTART);      // Дождаться пока ADSTART будет равен нулю

// Пугают! По остановам в цикле Всегда была ОДНА итерация

ADC1->ISR |= ADC_ISR_EOSEQ;             // Сбросить флаг окончания последовательности. Написано сбрасывается записью 1

ADC1->IER |= ADC_IER_EOSIE;             // Разрешить прерывания по концу последовательности каналов АЦП

ADC1->CR |= ADC_CR_ADSTART;             // ADC start of conversion

}

}

//------------------------------------------------------------------

void  ADC1_COMP_IRQHandler (void)    // Прерывания АЦП

{

if (ADC1->ISR & ADC_ISR_AWD)   {       // Прерывания от сторожевого таймера

Driver_Off      // Запретить работу ключей драйверов полевиков "оборвать провод"

TIM1->EGR |= TIM_EGR_BG;                // Установить флаг прерывания аварийного останова TIM1

ADC1->ISR |= ADC_ISR_AWD;             // Написано сбрасывается записью 1

}

//-----

if (ADC1->ISR & ADC_ISR_EOSEQ)   {       // Прерывания по концу последовательности преобразования каналов

ADC1->ISR |= ADC_ISR_EOSEQ;           // Написано сбрасывается записью 1

//!!!!!

ADC1->CR |= ADC_CR_ADSTP;               // ADC stop of conversion command

while (ADC1->CR & ADC_CR_ADSTART);      // Дождаться пока ADSTART будет равен нулю

// Пугают! По остановам в цикле Всегда была ОДНА итерация

ADC1->IER &= ~ADC_IER_EOSIE;            // запретить прерывания по концу последовательности каналов АЦП

ADC1->CFGR1 |= ADC_CFGR1_DMAEN | ADC_CFGR1_DMACFG;      // Включить передачу DMA на АЦП в круговом режиме

ADC1->CR |= ADC_CR_ADSTART;             // ADC start of conversion

DMA1_Channel1->CPAR = (uint32_t) (&(ADC1->DR));         //  Настроить адрес регистра периферийных данных

DMA1_Channel1->CMAR = (uint32_t)(&ADC1_Simpl[0]);       //  Настроить адрес памяти

DMA1_Channel1->CNDTR = T_N_Simpl;       //  Настроить количество байт DMA-передачи, которое должно выполняться по каналу DMA 1

DMA1_Channel1->CCR |= DMA_CCR_EN;                       // Включить канал DMA 1

DMA1->IFCR = DMA_IFCR_CGIF1;           // 1: очищает флаги GIF, TEIF, HTIF и TCIF в регистре DMA_ISR

DMA1_Channel1->CCR |= DMA_CCR_TCIE;                     //  Разрешение прерывания с полной передачей

}

}

//------------------------------------------------------------------

void  DMA1_Channel1_IRQHandler (void)    // Прерывания DMA ADC1

{

if (DMA1->ISR & DMA_ISR_TCIF1)   {    // Channel 1 Transfer Complete flag

DMA1_Channel1->CCR &= ~DMA_CCR_TCIE;    //  Запретить прерывания с полной передачей

DMA1->IFCR = DMA_IFCR_CGIF1;            // 1: очищает флаги GIF, TEIF, HTIF и TCIF в регистре DMA_ISR

DMA1_Channel1->CCR &= ~DMA_CCR_EN;      // Выключить канал DMA1

ADC1->CR |= ADC_CR_ADSTP;               // ADC stop of conversion command

ADC1->CFGR1 &= ~(ADC_CFGR1_DMAEN | ADC_CFGR1_DMACFG);      // Выключить передачу DMA на АЦП в круговом режиме

//ADC1->CFGR1 |= ADC_CFGR1_OVRMOD;        // Прекращаем читать данные.

ADC1->CR |= ADC_CR_ADSTART;             // ADC start of conversion

Sin_Average_GO = 1;

}

}

//------------------------------------------------------------------------------

void  TIM14_IRQHandler (void) // Прерывание от таймера TIM14

{

// 121 - количество приращений счетчика Tim2 с шагом Mod. У нас не бывает af = Act_F = 0

// От стопа до 100 оборотов изменение частоты не плавное, Поэтому все массивы счетчиков

// начинаются с незначащего нуля, а работа не с нулевого, а с первого элемента массива

static int af;

static int mf;

static long t2;

static int dst;

af = Act_F;

mf = Mod_F;

t2 = TIM2->ARR;

dst = Div_St_Cr;

if (TIM14->SR & TIM_SR_UIF)   {

TIM14->SR &= ~TIM_SR_UIF;   // Сбрасываем флаг прерывания

if (mf > af) {

if (Mod_St_Cr++ < Tim2_St_N)                      // Tim2_St_N = 121 - количество приращений счетчика Tim2 с шагом Mod.

{ t2 = t2 - Mod_Fr_Loc_Tim2[af];                  //

if ((dst++) < Div_Fr_Loc_Tim2[af]) t2--;        // Поскольку меняется шаг приращения, то остаток < Tim2_St_N в любом случает отрабатывается

}

else {

dst = 0; Mod_St_Cr = 0; af++;

t2 = Fr_Loc_Tim2 [af];

}

}

if (mf < af) {

if (Mod_St_Cr++ < Tim2_St_N)

{ t2 = t2 + Mod_Fr_Loc_Tim2[af-1];

if ((dst++) < Div_Fr_Loc_Tim2[af-1]) t2++;

}

else {

dst = 0; Mod_St_Cr = 0; af--;

t2 = Fr_Loc_Tim2 [af];

}

}

// Act_F = 7700. Mod_F = 7800. d_TIM2->ARR = 15584-15385 = 200

// (вычисления ведутся с плавающей точкой и округляются до целых в последнем результате)

// Это 121 раза уменьшить на Mod_Fr_Loc_Tim2[af] = 1 и Div_Fr_Loc_Tim2[af] = 79 (всегда меньше 121)

// В любом случае, при любых ошибках округления, плавность будет достигнута

// А окончательное значение TIM2->ARR = Fr_Loc_Tim2 [af];

// Act_F = 3000. Mod_F = 2900. d_TIM2->ARR = 41379-40000 = 1379

// Это 121 раз увеличить на Mod_Fr_Loc_Tim2[af-1] = 11 и Div_Fr_Loc_Tim2[af-1] = 48

// Mod и Div прибавляются (вычитаются) к TIM2->ARR в одном цикле. Mod > Div.

}

Div_St_Cr = dst;

TIM2->ARR = t2;

Act_F = af;

Power = 0;

}

//_____________________________________________________________________________

// НЕ ЗАБЫВАЕМ про тактирование

//Включить тактирование порта. Порты I/O по шине AHBENR

RCC->AHBENR |= RCC_AHBENR_GPIOAEN;

RCC->AHBENR |= RCC_AHBENR_GPIOBEN;

RCC->AHBENR |= RCC_AHBENR_GPIOCEN;

RCC->AHBENR |= RCC_AHBENR_GPIOFEN;

RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;     // Включить TIM1 по шине APB2ENR

RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;     // Включить TIM2 по шине APB1ENR

RCC->APB1ENR |= RCC_APB1ENR_TIM14EN;    // Включить TIM14 по шине APB1ENR

RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;   // Включили тактирование EXTI

RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;     // Включили тактирование SPI1

RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;     // Включили тактирование ADC

RCC->AHBENR |= RCC_AHBENR_DMA1EN;       // Включить периферийные часы на DMA

RCC->CR2 |= RCC_CR2_HSI14ON;                    // Активировать внутренний генератор HSI14 для АЦП

while ((RCC->CR2 & RCC_CR2_HSI14RDY) == 0)      // Дождаться

// RC-генератор HSI 14 МГц не может быть включен с помощью интерфейса АЦП, когда часы APB выбраны как часы ядра АЦП.

// HSI14-генератор (тактирование АЦП). Данный RC-генератор является одним из двух способов тактирования блока АЦП (ADC)

// (второй  способ – через тактовую частоту PCLK, делённую на 2 или 4)

//______________________________________________________________________________

// Настройка регистров таймеров

//------------------------------------------------------------------------------

// конфигурация таймера TIM1

// Генерация несущей. 3-фазный ШИМ с выравниванием по центру

// Установка предварительного делителя

//TIM1->PSC = PSC_Val;   //      F=fCK_PSC/(PSC[0:15]+1)

// В SM32 внутренний RC генератор с частотой 48 МГц.

// Установим предварительный делитель равным 32.

// Счетчик считае 2 раза (симметричный режим).

// Значения счетчика будут инкрементироваться с частотой 41 Кгц:

// Коэффициент деления равен 1 при нулевом значении предварительного делителя

// Для получения коэффициента деления N, необходимо установить в N-1.

// Установка границы счета

//TIM1->ARR = Sin_ordinate;

// Здесь и далее закомментированы некоторые настройки, которые обязательны,

// однако в проекте в целом включаются в других местах

// TIMx_ARR – регистр автоматической перезагрузки,

// счётчик считает от 0 до TIMx_ARR, или наоборот в зависимости

// от направления счёта, изменяя это значение, мы изменяем частоту ШИМ.

// Предварительная установка скважности

//TIM1->CCR1 = Sin_A [0];

//TIM1->CCR2 = Sin_B [0];

//TIM1->CCR3 = Sin_C [0];

// TIMx_CCRy[x – номер таймера, y – номер канала] – определяет коэффициент

// заполнения ШИМ. То есть, если в ARR мы запишем 1000, а в CCRy 300,

// то коэффициент заполнения при положительном активном уровне

// и прямом ШИМ будет равен 0.3 или 30%.

// Включаем режим канал в режим ШИМ

TIM1->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1;     // ШИМ режим 1 (OC1M = 110) | TIM_CCMR1_OC1M_0

TIM1->CCMR1 |= TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1;     // ШИМ режим 1 (OC1M = 110) | TIM_CCMR1_OC2M_0

TIM1->CCMR2 |= TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC3M_1;     // ШИМ режим 1 (OC1M = 110) | TIM_CCMR2_OC3M_0

// Это прямой ШИМ. Если записать "111" будет инверсный

TIM1->CR1 |= TIM_CR1_ARPE;//Включен режим предварительной записи регистра автоперезагрузки

TIM1->CCMR1 |= TIM_CCMR1_OC1PE; // Включаем регистр предварительной загрузки компатратора канала 1

TIM1->CCMR1 |= TIM_CCMR1_OC2PE; // Включаем регистр предварительной загрузки компатратора канала 2

TIM1->CCMR2 |= TIM_CCMR2_OC3PE; // Включаем регистр предварительной загрузки компатратора канала 3

//TIM1->CR1 |= TIM_CR1_DIR; // Если установить, будет считать вниз

TIM1->CR1 |= TIM_CR1_CMS_0 | TIM_CR1_CMS_1; // Режим 3 выравнивания по центру

// (Center-aligned mode 3). Счетчик считает вверх и вниз поочередно.

// Флаги прерывания сравнения выхода каналов, настроенных на выход

// (CCxS=00 в регистре TIMx_CCMRx) устанавливаются только тогда,

// когда счетчик считает вверх или вниз

// Select active high polarity on OC1 (CC1P = 0, reset value), enable the output on OC1 (CC1E = 1)

// Выберите активную высокую полярность в OC1 (CC1P = 0, значение сброса), включите выход на OC1 (CC1E = 1)

// CC1P & CC1NP - выбор полярности 0: активный уровень высокий - "1". 1: активный уровень низкий - "0".

TIM1->CCER |= TIM_CCER_CC1E | TIM_CCER_CC1NE;

TIM1->CCER |= TIM_CCER_CC2E | TIM_CCER_CC2NE;

TIM1->CCER |= TIM_CCER_CC3E | TIM_CCER_CC3NE;

TIM1->BDTR |= Deadtime;       // Мертвое время. Константа расчитана из задержек конкретного железа.

TIM1->BDTR |= TIM_BDTR_BKE | TIM_BDTR_BKP;      // break input, active polarity = high

//TIM1->BDTR |= TIM_BDTR_MOE;     // Разрешаем вывод сигнала на выводы

//TIM1->EGR |= TIM_EGR_UG;         // генерация обновления

//TIM1->DIER |= TIM_DIER_UIE;      // Разрешить прерывание по переполнению

//TIM1->CR1 |= TIM_CR1_CEN; // Запускаем счет

//NVIC_EnableIRQ(TIM1_BRK_UP_TRG_COM_IRQn);

NVIC->ISER[0] = (1 << ((TIM1_BRK_UP_TRG_COM_IRQn) & 0x1F)); // Разрешить TIM1_BRK_UP_TRG_COM_IRQn, если что № 13  & 0x1F

// NVIC->ISER[1] |= 0x00000002; // Разрешить IRQ33, если бы у нас было больше 30 векторов. ))))

//NVIC_SetPriority(TIM1_BRK_UP_TRG_COM_IRQn, 1);

//NVIC->IP[3] |= 0x00004000; // Приоритет TIM1_BRK_UP_TRG_COM_IRQn - ОДИН

//NVIC->IP[M] M = 13 DIV 4 = 3

//N = 13 MOD 4 = 1. Байты о-5 обнуляются контроллером.

//Остаются байты 6-7 под 4 уровня приоритета 0x0000С000 - приоритет ТРИ.

//i = NVIC_GetPriority(TIM1_BRK_UP_TRG_COM_IRQn);

//NVIC_SetPriority(TIM1_BRK_UP_TRG_COM_IRQn, 1);

//------------------------------------------------------------------------------

// конфигурация таймера TIM2

// Установка предварительного делителя

//TIM2->PSC = 0;  // F=fCK_PSC/(PSC[0:15]+1) 100000

// Установка границы счета

//TIM2->ARR = Fr_Loc_Tim2 [0];      // TIMx_ARR – регистр автоматической перезагрузки,

TIM2->CCR3 &= ~(0xFFFF0000);            // Подчеркнем, что счетчик 32-х разрядный

TIM2->CCR3 = Tim2_Start_AMA_ADC;        // Грузим в компататор значение включения АЦП

TIM2->CR1 |= TIM_CR1_ARPE;        //Включен режим предварительной записи регистра автоперезагрузки

TIM2->CCMR2 |= TIM_CCMR2_OC3PE;         // Включаем регистр предварительной загрузки компаратора 3

TIM2->DIER |= TIM_DIER_UIE;       // Разрешить прерывание по переполнению

TIM2->DIER |= TIM_DIER_CC3IE;           // Разрешить прерывание по совпадению

//TIM2->EGR |= TIM_EGR_UG;        // генерация обновления

//TIM2->CR1       |= TIM_CR1_CEN; // Запускаем счет

NVIC->ISER[0] = (1 << ((TIM2_IRQn) & 0x1F));     // Разрешить TIM2_IRQn  & 0x1F

//NVIC_SetPriority(TIM2_IRQn, 1);                // Приоритет TIM3_IRQn - ТРИ

//k = NVIC_GetPriority(TIM2_IRQn);

//------------------------------------------------------------------------------

// конфигурация таймера TIM14

// Установка предварительного делителя

TIM14->PSC = Tim14_Acc_PSC - 1; // 1200 Подделитель счетчика разгона Tim14

// Установка границы счета

TIM14->ARR = Tim14_Acc_ARR - 1; // 40000 TIM14->ARR счетчика разгона Tim14

// Эта установка для секундного интервала плавного пуска питания

TIM14->CR1 |= TIM_CR1_ARPE;     //Включен режим предварительной записи регистра автоперезагрузки

TIM14->EGR |= TIM_EGR_UG;       // генерация обновления

TIM14->DIER |= TIM_DIER_UIE;    // Разрешить прерывание по переполнению

//TIM14->EGR |= TIM_EGR_UG;        // генерация обновления

//TIM14->CR1       |= TIM_CR1_CEN; // Запускаем счет

//NVIC_EnableIRQ(TIM3_IRQn);

NVIC->ISER[0] = (1 << ((TIM14_IRQn) & 0x1F));     // Разрешить TIM14_IRQn  & 0x1F

//NVIC_SetPriority(TIM14_IRQn, 1);                // Приоритет TIM14_IRQn - ДВА

//j = NVIC_GetPriority(TIM14_IRQn);

//______________________________________________________________________________

// Настройка регистров ADC & DMA

//------------------------------------------------------------------------------

// Настройка DMA

//  Включить передачу DMA на АЦП и круговом режиме

//Примечание: бит DMAEN в регистре ADC_CFGR1 должен быть установлен после фазы калибровки ADC

//      В этом примере, взятом из мануала, не было написано кого сначала нужно настраивать.

//      Это привело к тому, что запись DMA1 начиналась не с первого, а с последнего датчика регулярной группы!!!

//DMA1_Channel1->CPAR = (uint32_t) (&(ADC1->DR));                 //  Настроить адрес регистра периферийных данных

//  В регистре SYSCFG прописано какой канал для какого устройства можно юзать см. Тав 29 мануала

//DMA1_Channel1->CMAR = (uint32_t)(&ADC1_Simpl[0]);                //  Настроить адрес памяти

//DMA1_Channel1->CNDTR = T_N_Simpl;               //  Настроить количество байт DMA-передачи, которое должно выполняться по каналу DMA 1

//  NDT [15: 0]: Количество передаваемых данных (от 0 до 65535). Этот регистр может быть записан только в том случае,

//  если канал отключен. Как только канал включен, этот регистр доступен только для чтения, указывая оставшиеся байты,

//  которые должны быть переданы. Этот регистр уменьшается после каждой передачи DMA

DMA1_Channel1->CCR |= DMA_CCR_MINC | DMA_CCR_MSIZE_1 | DMA_CCR_PSIZE_0;

//      Не совсем понятно, почему в примере из мануала стоял DMA_CCR_MSIZE_0

//      Если данные считываются в память - массив int [n], то они 32-х разрядные.

//DMA_CCR_MEM2MEM       Memory to memory mode                   Hежим памяти в память

//      0: режим памяти в память отключен

//      1: включен режим памяти в память

//DMA_CCR_PL            Bits(Channel Priority level             Уровень приоритета канала

//      00: Низкий

//      01: Средний

//      10: Высокий

//      11: Очень высокий

//DMA_CCR_MSIZE         MSIZE[1:0] bits (Memory size)           Размер памяти

//      00: 8 бит

//      01: 16 бит

//      10: 32 бит

//DMA_CCR_PSIZE         PSIZE[1:0] bits (Peripheral size)       Размер периферии

//      00: 8 бит

//      01: 16 бит

//      10: 32 бит

//DMA_CCR_MINC          Memory increment mode                   Инкрементный режим адреса памяти

//      0: Режим увеличения памяти отключен

//      1: включен режим увеличения памяти

//DMA_CCR_PINC          Peripheral increment mode               Инкрементный режим адреса периферии

//      0: отключен периферийный режим увеличения

//      1: включен режим расширенного периферийного устройства

//DMA_CCR_CIRC          Circular mode                           Циклический режим

//DMA_CCR_DIR           Data transfer direction                 Направление передачи данных

//      0: чтение с периферийных устройств

//      1: чтение из памяти

//DMA1_Channel1->CCR |= DMA_CCR_TEIE;                             //  Разрешение прерывания ошибки передачи

//      DMA_CCR_TEIE          Transfer error interrupt enable   Разрешение прерывания ошибки передачи

//      0: прерывание TE отключено

//      1: прерывание TE включено

//DMA1_Channel1->CCR |= DMA_CCR_HTIE;                             //  Разрешение прерывания с половинной передачей

//      DMA_CCR_HTIE    Half Transfer interrupt enable          Разрешение прерывания с половинной передачей

//DMA1_Channel1->CCR |= DMA_CCR_TCIE;                             //  Разрешение прерывания с полной передачей

//      DMA_CCR_TCIE    Transfer complete interrupt enable      Разрешение прерывания с полной передачей

//DMA1_Channel1->CCR |= DMA_CCR_EN;                               //  Включить канал DMA 1

//      DMA_CCR_EN      Channel enable                          Включить канал

//NVIC_EnableIRQ(DMA1_Channel1_IRQn);             // Разрешить прерывания DMA ADC

//NVIC->ISER[0] = (1 << ((DMA1_Channel1_IRQn) & 0x1F));

//------------------------------------------------------------------------------

// Настройка ADC

RCC->CR2 |= RCC_CR2_HSI14ON;                    // Активировать внутренний генератор HSI14 для АЦП

while ((RCC->CR2 & RCC_CR2_HSI14RDY) == 0)      // Дождаться

// RC-генератор HSI 14 МГц не может быть включен с помощью интерфейса АЦП, когда часы APB выбраны как часы ядра АЦП.

// HSI14-генератор (тактирование АЦП). Данный RC-генератор является одним из двух способов тактирования блока АЦП (ADC)

// (второй  способ – через тактовую частоту PCLK, делённую на 2 или 4)

// Выключение. В мануале рекомендуют выключать прежде чем включать на всякий пожаоный.

if ((ADC1->CR & ADC_CR_ADEN) != 0) ADC1->CR |= ADC_CR_ADDIS;    // Если включен, выключить

while ((ADC1->CR & ADC_CR_ADEN) != 0) asm("nop");               // Дождаться

ADC1->CFGR1 &= ~ADC_CFGR1_DMAEN;                                // Выключить

// ADC_CR_ADEN - ADC enable control (Управление ADC включено)

// ADC_CR_ADDIS - ADC disable command (Команда отключения ADC)

// ADC_CFGR1_DMAEN - Direct memory access enable (Прямой доступ к памяти включен)

// Калибровка

ADC1->CR |= ADC_CR_ADCAL;                                       // Калибровка АЦП

while ((ADC1->CR & ADC_CR_ADCAL) != 0) asm("nop");              // Дождаться

// ADC_CR_ADCAL - ADC calibration (Калибровка АЦП)

// Включение

ADC1->CR |= ADC_CR_ADEN;

while ((ADC1->ISR & ADC_ISR_ADRDY) == 0) asm("nop");

// ADC_ISR_ADRDY - ADC Ready (Управление ADC включено)

// ADC_CR_ADEN - ADC enable control (Управление ADC включено)

// Сторожевой таймер. Выше еще и обработчик прерываний, аналогичный кнопке "Стоп"

ADC1->CFGR1 |= ADC_CFGR1_AWDEN;         //  Включить сторожевой таймер во всей регулярной группе

ADC1->CFGR1 &= ~ADC_CFGR1_AWDSGL;       //  Включить сторожевой таймер по отдельному каналу

ADC1->TR = (vrefint_high << 16) + vrefint_low;          // 3686 = 4095-410 (от 2,97 до 0,33 В)

ADC1->IER |= ADC_IER_AWDIE;           //  Разрешить прерывания от сторожевого таймера

// Бит 27:16 HT [11: 0]: Высокий порог аналогового сторожевого таймера

// Бит 11: 0 LT [11: 0]: Нижний порог аналогового сторожевого таймера

//ADC1->CFGR1 |= ADC_CFGR1_DISCEN;      // Прерывистый режим. Для запуска каждого преобразования,

// определенного в последовательности, требуется событие запуска оборудования или программного обеспечения

//ADC1->CFGR1 |= ADC_CFGR1_AUTOFF;        // Автоматически отключать по окончании последовательности

//ADC1->CFGR1 |= ADC_CFGR1_WAIT;        // Преобразование режима ожидания. Новое преобразование может начаться

// только в том случае, если предыдущие данные были обработаны, как только регистр ADC_DR был прочитан или бит EOC был очищен

// Штука приятная и позиций кода и потребления энергии. Но какналов в последовательности может быть всего 3 (Три).

// Может быть полезно разрешить преобразование одного или нескольких каналов ADC без чтения данных после каждого преобразования.

// В этом случае бит OVRMOD должен быть сконфигурирован в 1, а флаг OVR должен быть проигнорирован программным обеспечением.

// Когда OVRMOD = 1, событие переполнения не препятствует продолжению преобразования ADC, и регистр ADC_DR всегда содержит последние данные преобразования.

ADC1->CFGR1 |= ADC_CFGR1_OVRMOD;        // Обслуживаем только сторожевой таймер. Он работает до чтения данных.

// OVRMOD     0: Регистр ADC_DR сохраняется со старыми данными при обнаружении переполнения.

// OVRMOD     1: Регистр ADC_DR перезаписывается последним результатом преобразования при обнаружении переполнения.

//  Включить АЦП в круговом режиме

ADC1->CFGR1 |= ADC_CFGR1_CONT;

//  ADC_CFGR1_DMAEN     Когда режим DMA включен, запрос DMA генерируется после преобразования каждого канала

//  ADC_CFGR1_DMACFG    Кольцевой режим DMA (DMACFG = 1)

//  ADC_CFGR1_CONT      Непрерывное преобразование

//ADC1->CFGR2 &= ~ADC_CFGR2_CKMODE;     // Выберите HSI14, записав 00 в CKMODE (значение сброса)

// Источник тактирования                CKMODE[1:0]     Задержка между событием триггера

//                                                      и началом преобразования

// Выделенный 14 МГц тактовый сигнал            00      Задержка не является детерминированной (дребезг)

// PCLK, деленный на 2                          01      Задержка детерминирована (без дребезга)

//                                                      и равна 2.75 тактов ADC

// PCLK, деленный на 4                          10      Задержка является детерминированной (без дребезга)

//                                                      и равна 2.625 тактам ADC

// Выбор регулярной группы

ADC1->CHSELR = ADC_CHSELR_CHSEL10 | ADC_CHSELR_CHSEL11 | ADC_CHSELR_CHSEL12 | ADC_CHSELR_CHSEL13;      // Выбрать каналы

//ADC1->CFGR1 ^= ADC_CFGR1_SCANDIR;    // изменить порядок опроса

// Порядок, в котором каналы будут сканироваться, можно настроить, запрограммировав бит бит SCANDIR в регистре ADC_CFGR1:

//      SCANDIR = 0: прямое сканирование. Канал 0 — канал 18

//      SCANDIR = 1: обратное сканирование. Канал 18 — канал 0

// Датчик температуры подключен к каналу ADC_IN16.

// Внутренний опорный сигнал напряжения VREFINT подключен к каналу ADC_IN17.

// Канал VBAT подключен к каналу ADC_IN18.

//ADC1->CHSELR = ADC_CHSELR_CHSEL18;// Выбрать VREFINT

// Как-то странно работает... в составе регулярной группы. Такое чувство, что его нужно опрашивать отдельно

ADC1->SMPR |= ADC_SMPR1_SMPR_0 | ADC_SMPR1_SMPR_2;      //  ADC_SMPR1_SMPR_0 | ADC_SMPR1_SMPR_1 | ADC_SMPR1_SMPR_2;

//      000:    1,5     тактов ADC

//      001:    7.5     ADC тактов

//      010:    13,5    тактов ADC

//      011:    28,5    тактовые циклы ADC

//      100:    41,5    тактов ADC

//      101:    55,5    тактов ADC

//      110:    71,5    тактов ADC

//      111:    239.5   Циклы синхронизации ADC

// Это время выборки должно быть достаточно для того, чтобы источник входного напряжения заряжал образец

// и удерживал конденсатор до уровня входного напряжения.

// С одним из 4 каналов в воздухе 55,5 тактов хватает

//ADC->CCR |= ADC_CCR_VREFEN; // Wake-up the VREFINT (only for VBAT, Temp sensor and VRefInt)

//Пробуждение VREFINT (только для VBAT, Temp sensor и VRefInt)

//______________________________________________________________________________________________

В данной статье я не претендую на звание ГУРУ. Просто собрал в одном месте описания устройств.

Так, например, у меня не получилось использовать прерывание по половине буфера DMA ADC, вернее его разрешение и запрещение. Поставил в обработчики счетчики. Как ни бился, количество прерываний по окончанию полвины буфера и по окнчанию буфера получалось разным.

В F0 с использованием CMSIS получается достаточно элегантно загрузить камушек. Пусть это будет мое мноение. В микроконтроллерах F4, F7, H7 я использую CubeMx.

С уважением Петр.

Реализуем коммерческие проекты.
Возможна работа по договору подряда.

t654rk@mail.ru

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.