STM32, C++ и FreeRTOS. Разработка с нуля. Часть 2

от автора


Введение

В прошлой публикации STM32, C++ и FreeRTOS. Разработка с нуля. Часть 1 я остановился на том, как уехал на озеро как были релизованы требования SR7, SR4 и SR6. Напомню, какие требования вообще есть для проекта:
SR0: Устройство должно измерять три параметра (иметь три переменных): Температуру микропроцессора, Напряжение VDDA, Напряжение с переменного резистора
SR1: Устройство должно выводить значение этих переменных на индикатор.
SR2: Единицы измерения для Температуры микропроцессора — градусы Цельсия, для остальных параметров — вольты.
SR3: При нажатии на кнопку 1, на индикаторе должен показываться экран со следующей измеряемой переменной,
SR4: При нажатии на кнопку 1 Светодиод 1 должен изменять свое состояние
SR5: При нажатии на кнопку 2, на индикаторе должен поменяться режим отображения переменных с постоянного показывания переменной на последовательное (менять экраны раз в 1.5 секунды) при следующем нажатии с последовательного на постоянное,
SR6: При нажатии на кнопку 2 светодиод 2 должен менять свое состояние.
SR7: Светодиод 3 должен моргать раз в 1 секунду.

Разработка: АЦП

Решив что я постиг все примудрости новых микроконтроллеров, я решил взять самое амбициозное требование SR0 — собственно это и есть основной функционал устройства — измерять 3 величины.
Для начала нужно было разобраться с АЦП. Решив взять этот блок с лету, особо не читая документацию на микроконтроллер, воооружившись специальным тулом Crt-C и Ctr-V, я нарисовал копию архитектур управления светодиодами и кнопок.

image

Но начав реализовывать сей чудный рисунок, который в общем-то очень даже рабочий, увлекся чтением документации и понял, что сделать можно вообще без активного объекта, используя канал DMA. Конечно такая архитектура уже будет процессоро-зависимой, по той простой причине, что не все микроконтроллеры имеют такой блок, но я подумал, что будет интересно и полезно показать, то, как можно немного все упростить, используя особенности микроконтроллера.
И переделал все вот так:

image

Все архитектура готова, и тут я завис. Оказалось что настроить АЦП немного сложнее чем порты, да и у меня упорно не измерялось напряжение с переменного резистора. Температура есть, Vdda есть, а с переменника никак. Настроить АЦП помог опять тот же ресурс, что помог мне сделать проект STM32L. ADC — Аналого-цифровой преобразователь и STM32L. Контроллер DMA. А разобраться с переменником демо-проект, скачанный с документацией для платы Olimex. Оказалось, что его просто надо было подключить отдельной ножкой PortD.Pin1 процессора. Как обычно, всю настройку железа я выкинул в __low_level_init()

Настройк АЦП и DMA

  //включаем потенциометр(Триммер) PORTD_PIN1     GPIOD->MODER |= GPIO_MODER_MODER1_0;    GPIOD->PUPDR |= GPIO_PUPDR_PUPDR1_0;    GPIOD->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR1;    //настраиваем АЦП, 12 бит, канал 16- температурный сенсор, 17 - VDDA,    // 22 - триммер в континиус режиме,     //регулярные каналы, скан каналов, ожидание следующего измерения, пока не скинут    // EOC, установка EOC после после серии измерений, см http://chipspace.ru/stm32l-discovery-adc/    // В итоге мы будем мерить последовательно каналы 16(температуру) и 17(vdda) и       // 22(триммер)первое преобразование будет температура, 2- vdda, 3- триммер    ADC1->CR2 |= (ADC_CR2_DELS_2 | ADC_CR2_CONT);    ADC1->CR1 |= ADC_CR1_SCAN;       //Порт GPIOE.7 как аналоговый вход - триммер     GPIOE->MODER |= GPIO_MODER_MODER7;    //3 измерения       ADC1->SQR1 |= ADC_SQR1_L_1;    //Выбираем ADC_IN 16 для 1 преобразования, стр 305    //Выбираем ADC_IN 17 для 2 преобразования, стр 305    //Выбираем ADC_IN 22 для 3 преобразования, стр 305    ADC1->SQR5 |= ADC_SQR5_SQ1_4 | ADC_SQR5_SQ2_0 | ADC_SQR5_SQ2_4 | ADC_SQR5_SQ3_1 | ADC_SQR5_SQ3_2 | ADC_SQR5_SQ3_4;    //Выбираем время преобразование для 16 и 17  и 22 канала стр 301 и 279    ADC1->SMPR2 |= ADC_SMPR2_SMP16 | ADC_SMPR2_SMP17_2;    ADC1->SMPR1 |= ADC_SMPR1_SMP22_2;    // Включаем внутренние входы каналов измреения температурного сенсора и VDDA       ADC->CCR |= ADC_CCR_TSVREFE;      // DMA    ADC1->CR2 |= (ADC_CR2_DMA | ADC_CR2_DDS);    //Настройка DMA    //Направление передачи данных - чтение из периферии, запись в память.    DMA1_Channel1->CCR &= ~DMA_CCR1_DIR;      //Адрес периферии не инкрементируется после каждой пересылки.     DMA1_Channel1->CCR &= ~DMA_CCR1_PINC;    //Адрес памяти инкрементируется после каждой пересылки.     DMA1_Channel1->CCR |= DMA_CCR1_MINC;     //Размерность данных периферии - 16 бит.    DMA1_Channel1->CCR |= DMA_CCR1_PSIZE_0;     //Размерность данных памяти - 16 бит    DMA1_Channel1->CCR |= DMA_CCR1_MSIZE_0;    //Приоритет - очень высокий (Very High)    DMA1_Channel1->CCR |= DMA_CCR1_PL;     DMA1_Channel1->CCR |= DMA_CCR1_CIRC;     

Сами файлы реализации классов:

adc.h

#include "types.h"            //Стандартные типы проекта #define SENSORTEMPERATURE_CHANNEL       0 #define VDDA_CHANNEL                    1  #define TRIMMER_CHANNEL                 2 class cAdc {   public:     explicit  cAdc(const tU32 memoryBaseAddr, const tU8 measureCount);     tBoolean switchOn(void);     tBoolean startConversion(void);     tBoolean isConversionReady(void);     tF32 getValue(void) const;   private:     void initDma(const tU32 memoryBaseAddr, const tU8 measureCount); }; 

adc.cpp

#include <stm32l1xx.h>      // Регистры STM2 #include "adc.h"                  // Описание класса #include "susuassert.h"      //for ASSERT #include "bitutil.h"               //для макросов работы с битами SETBIT, CLRBIT #define ADC1_DR_ADDRESS    ((tU32)0x40012458) /******************************************************************************* * Function:  constructor * Description: инициализиурет канал DMA адресом в RAM, куда складывать данные *              измерений и количеством измерений ******************************************************************************/ cAdc::cAdc(const tU32 memoryBaseAddr, const tU8 measureCount) {   ASSERT(measureCount != 0);    this->initDma(memoryBaseAddr, measureCount); } /******************************************************************************* * Function:  switchOn * Description: Включает АЦП ******************************************************************************/ tBoolean cAdc::switchOn(void) {   tBoolean  result = FALSE;   //Включаем АЦП, стр 299 CD00240194.pdf   SETBIT(ADC1->CR2, ADC_CR2_ADON);   result =  tBoolean(CHECK_BIT_SET(ADC1->SR, ADC_SR_ADONS));   return result;      } /******************************************************************************* * Function:  startConversion() * Description: Запускает преобразование ******************************************************************************/ tBoolean cAdc::startConversion(void) {   tBoolean  result = FALSE;   //Запускаем преобразование АЦП, стр 299 CD00240194.pdf   SETBIT(ADC1->CR2, ADC_CR2_SWSTART);   result = tBoolean(CHECK_BIT_SET(ADC1->SR, ADC_SR_STRT));   return result; } /******************************************************************************* * Function:  getValue() * Description: читаем результат преобразования ******************************************************************************/ tF32 cAdc::getValue(void) const {   tF32  result = ADC1->DR;   return result;  } /******************************************************************************* * Function:  isConversionReady() * Description: готово ли преобразование? ******************************************************************************/ tBoolean cAdc::isConversionReady(void) {   tBoolean result = tBoolean(CHECK_BIT_SET(ADC1->SR, ADC_SR_EOC));   return result; } /******************************************************************************* * Function:  initDma() * Description: инициализирует канал DMA ******************************************************************************/ void cAdc::initDma(const tU32 memoryBaseAddr, const tU8 measureCount) {   //Задаем адрес периферии - регистр результата преобразования АЦП для регулярных каналов.    DMA1_Channel1->CPAR = ADC1_DR_ADDRESS;   //Задаем адрес памяти - базовый адрес массива в RAM.   DMA1_Channel1->CMAR = memoryBaseAddr;   DMA1_Channel1->CNDTR = measureCount;   //Включаем DMA   SETBIT(DMA1_Channel1->CCR, DMA_CCR1_EN);   } 

adcdirector.h

#include "adc.h"              //для класса cAdc #define MEASUR_NUMBER       (tU8) 3 class cAdcDirector  {   public:     explicit  cAdcDirector(void);     void startConversion(void);     __IO uint16_t channelValue[MEASUR_NUMBER];   // для хранения преобразований   private:     cAdc *pAdc;     }; 

adcdirector.cpp

#include "adcdirector.h"  //Описание класса  /******************************************************************************* * Function:  constructor * Description: создает экземпляр АЦП, и передает ему адреса в RAM, куда АЦП с  *              помощью DMA будет скалдывать результат преобразований.  ******************************************************************************/ cAdcDirector::cAdcDirector(void) {   this->pAdc = new cAdc((tU32)&channelValue[0], MEASUR_NUMBER);   this->pAdc->switchOn();    } /******************************************************************************* * Function:  startConversion * Description: Запускаем АЦП на измерение, все данные сыплятся по DMA в массив *              channelValue * Threading usage and Assumptions:  ******************************************************************************/ void cAdcDirector::startConversion(void) {   this->pAdc->startConversion();      } 

Проверить работу можно было только под отладчиком, потому что вывода на индиктор у меня пока нет. Но перед этим нужно добавить создание нового экзмпляра класса в main()

main()

void main( void ) {     //задача ButtonControllera должна оповещать другие задачи о нажатии   //на кнопку, и передавать её значение. Для этого заводим массив указателей на    //задачи, которые надо оповещать   static tTaskHandle tasksToNotifyFromButton[BUTTON_TASKS_NOTYFIED_NUM];   cAdcDirector *pAdcDirector = new cAdcDirector();   pAdcDirector->startConversion();   cLedsDirector *pLedsDirector = new cLedsDirector();   oRTOS.taskCreate(pLedsDirector, LEDSDIRECTOR_STACK_SIZE, LEDSDIRECTOR_PRIORITY, "Leds");    tasksToNotifyFromButton[LEDS_TASK_HANDLE_INDEX] = pLedsDirector->taskHandle;   cButtonsController *pButtonsController =  new cButtonsController(tasksToNotifyFromButton, BUTTON_TASKS_NOTYFIED_NUM);   oRTOS.taskCreate(pButtonsController, BUTTONSCONTROLLER_STACK_SIZE, BUTTONSCONTROLLER_PRIORITY, "Buttons");      oRTOS.startScheduler(); } 

Запустил на отладку и получил следующую картинку: Как раз видно, что все 3 значения в массиве channelValue[] поменялись и выделены красным. Проверять значения не стал, но на вскидку — что-то примерно похожее.

image

По обыкновению проект был сохранен тут: АЦП, кнопки и светодиоды в IAR 6.50

Разработка: Переменные

И так АЦП вроде бы работает, настало время превратить груду этих единиц и ноликов в что-то понятное людям, а имеено в температуру и напряжение:
Для начала я продумал, единый интерфейс для всех переменных. Здесь всего один виртуальный метод — собтсвенно расчет и один метод получения рассчитанного значения.
image

А далее нарисовал как могла бы выглядеть температура:

image

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

image

Класс температуры рализует метод расчета интерфейса. Но тут следует сделать ремарку, для расчета температуры используются заводские коэффициенты зашитые в микроконтроллере, и по идее, чтобы можно было портировать данный код на другую платформу надо бы сделать класс для доступа к энергонезависимым параметрам и передавать ссылку на этот класс классам, которым эти коэффициенты нужны, в данном случае температуре. Но времени у меня в обрез и делать городульку из-за трех коэффциентов очень не хотелось, поэтому спишем такой косяк на нехватку времени и оставим галочку в уме, что тут портация не выйдет(ну и ладно :)). Рализация всего этого дела выглядит так:

ivariable.h

#include "types.h"            //Стандартные типы проекта #include "adcdirector.h"      //для класса cAdcdirector class iVariable  {   public:     explicit  iVariable(const cAdcDirector *pAdcDirector);     virtual tF32 calculate(void) = 0;     tF32 getValue(void) const {return value;};     protected:     const cAdcDirector *pAdcDirector;     tF32 value;    }; 

ivariable.cpp

#include "ivariable.h"      //Описание класса  #include "susuassert.h"     // for ASSERT /******************************************************************************* * Function:  constructor * Description:  ******************************************************************************/ iVariable::iVariable(const cAdcDirector *pAdcDirector) {   ASSERT(pAdcDirector != NULL);   this->pAdcDirector = pAdcDirector;   this->value = 0.0F; } 

ifilter.h

#include "types.h"            //Стандартные типы проекта class iFilter  {   public:     explicit iFilter(void);     virtual tF32 filter(const tF32 previousValue,                          const tF32 currentValue, tF32 filterConst); }; 

ifilter.h

#include "susuassert.h"       // for ASSERT #include "types.h"            // для типов проекта #include "ifilter.h"          // описание класса /******************************************************************************* * Function:  constructor * Description: Задает порты и пины для 4-ех индикаторов ******************************************************************************/ iFilter::iFilter(void)   { } /******************************************************************************* * Function:  filter * Description: Функция фильтрации ******************************************************************************/ tF32 iFilter::filter(const tF32 previousValue, const tF32 currentValue, tF32 filterConst) {   ASSERT(filterConst != 0);   tF32 filteredValue = previousValue;   filteredValue = filteredValue + (currentValue - filteredValue) / filterConst;   return filteredValue; } 

temperature.h

#include "types.h"            //Стандартные типы проекта #include "adcdirector.h"      //для класса cAdcdirector #include "ifilter.h"          //для интрефейса iFilter #include "iVariable.h"        //для интрефейса iVariable class cTemperature : public iVariable, private iFilter  {   public:     explicit cTemperature(cAdcDirector *pAdcDirector);       tF32 calculate(void); }; 

temperature.cpp

#include "temperature.h"  //Описание класса  //Разница 110С - 30С (температура в точках калибровки), см стр 289 #define DELTA_110_30  80.0F  //процессор нагревается сам немного, поэтому коррекция на 28 градусов, а не на 30 :) #define DEGREE_30     28.0F   //Адрес коэффицента калибровки 2 стр 102 CD00277537.pdf #define TS_CAL2_ADDR   0x1FF8007C   //Адрес коэффицента калибровки 1 стр 102 CD00277537.pdf #define TS_CAL1_ADDR   0x1FF8007A   //Адрес кода VDDA при 3.0 В #define VDDA_CAL_ADDR  0x1FF80076   #define FILTER_CONST   20.0F  /******************************************************************************* * Function:  constructor * Description:  ******************************************************************************/ cTemperature::cTemperature(cAdcDirector *pAdcDirector) : iVariable(pAdcDirector)   { } /******************************************************************************* * Function:  calculate * Description: Расчет температуры ******************************************************************************/ tF32 cTemperature::calculate(void) {   tF32 temperature = 0.0F; //измеренная температура по одному отсчету АЦП    tF32 vdda = 0.0F;   //значение кода vdda   //коэффициенты калибровки температурного сенсора, см стр 289 CD00240193.pdf и   //стр 102 CD00277537.pdf   tF32 tsCal2 = (tF32)(*((tU32 *)(TS_CAL2_ADDR)) >> 16);    tF32 tsCal1 = (tF32) (*((tU32 *)(TS_CAL1_ADDR )));   tF32 vddaCal = (tF32)(*((tU32 *)(VDDA_CAL_ADDR)) >> 16);   temperature = (tF32)this->pAdcDirector->channelValue[SENSORTEMPERATURE_CHANNEL];   vdda = (tF32)this->pAdcDirector->channelValue[VDDA_CHANNEL];   //поскольку все коэффициенты были получены на производсве при 3.0 В VDDA,    //нам необходимо сделать коррекцию на наше значение vdda, остальное   //формула со см стр 289 CD00240193.pdf    temperature = DELTA_110_30 * ((temperature * vddaCal)/vdda -  tsCal1) /                                  (tsCal2 - tsCal1) + DEGREE_30;   this->value = this->filter(this->value, temperature, FILTER_CONST);    return  this->value;        } 

Теперь нужно сделать активный объект который будет переодически вызывать расчет температуры. Да ивообще в последствии будет контейнером для перменных, через него мы будем иметь доступ к переменным. Холст-кисть и вуаля:

image

Реализация проста до безобразия:

variablesdirector.h

#include "iActiveObject.h"    //Для интерфейса iActiveObject #include "temperature.h"      //для класса cTemperature class cVariablesDirector : public iActiveObject {   public:     explicit cVariablesDirector(cAdcDirector* pAdcDirector);     void run(void);     cTemperature *pTemperature;     }; 

variablesdirector.cpp

#include "variablesdirector.h"  // описание класса  #include "frtoswrapper.h"       // для oRTOS #include "susuassert.h"         // для ASSERT #define VARIABLESDIRECTOR_DELAY (tU32)40/portTICK_PERIOD_MS /******************************************************************************* * Function:  constructor * Description: включает АЦП ******************************************************************************/ cVariablesDirector::cVariablesDirector(cAdcDirector* pAdcDirector) {   ASSERT(pAdcDirector != NULL);   this->pTemperature = new cTemperature(pAdcDirector); } /******************************************************************************* * Function:  run * Description: Задача  расчета температуры ******************************************************************************/ void cVariablesDirector::run(void) {   for(;;)   {     this->pTemperature->calculate();     oRTOS.taskDelay(VARIABLESDIRECTOR_DELAY);   } } 

Осталась самое малое — запустить и проверить. Перед этим конечно же надо создать еще одну задачу в main()

main()

#include <stm32l1xx.h>          // Регистры STM2 #include "ledsdirector.h"       // Для класса cLedsDirector #include "buttonscontroller.h"  // Для класса cButtonsController #include "types.h"              // Для типов проекта #include "frtoswrapper.h"       // для cRtos #include "variablesdirector.h"  // Для cVariablesDirector #define LEDS_TASK_HANDLE_INDEX          0 #define BUTTON_TASKS_NOTYFIED_NUM       1 #define LEDSDIRECTOR_STACK_SIZE configMINIMAL_STACK_SIZE #define LEDSDIRECTOR_PRIORITY (tU32)2 #define BUTTONSCONTROLLER_STACK_SIZE 256//configMINIMAL_STACK_SIZE #define BUTTONSCONTROLLER_PRIORITY (tU32)3 #define VARIABLESDIRECTOR_STACK_SIZE (tU16) configMINIMAL_STACK_SIZE #define VARIABLESDIRECTOR_PRIORITY (tU32)2 // Не охота было заморачиваться с синглтоном, сделал oRTOS глобальным объектом // можно было конечно сделать сRTOS статическим, но че-то тоже заморочек много // зато просто, все равно всем нужен :) cRTOS oRTOS; .... void main( void ) {     //задача ButtonControllera должна оповещать другие задачи о нажатии   //на кнопку, и передавать её значение. Для этого заводим массив указателей на    //задачи, которые надо оповещать   static tTaskHandle tasksToNotifyFromButton[BUTTON_TASKS_NOTYFIED_NUM];   cAdcDirector *pAdcDirector = new cAdcDirector();   pAdcDirector->startConversion();   cVariablesDirector *pVariablesDirector = new cVariablesDirector(pAdcDirector);    oRTOS.taskCreate(pVariablesDirector, VARIABLESDIRECTOR_STACK_SIZE, VARIABLESDIRECTOR_PRIORITY, "Var");   cLedsDirector *pLedsDirector = new cLedsDirector();   oRTOS.taskCreate(pLedsDirector, LEDSDIRECTOR_STACK_SIZE, LEDSDIRECTOR_PRIORITY, "Leds");    tasksToNotifyFromButton[LEDS_TASK_HANDLE_INDEX] = pLedsDirector->taskHandle;   cButtonsController *pButtonsController =  new cButtonsController(tasksToNotifyFromButton, BUTTON_TASKS_NOTYFIED_NUM);   oRTOS.taskCreate(pButtonsController, BUTTONSCONTROLLER_STACK_SIZE, BUTTONSCONTROLLER_PRIORITY, "Buttons");      oRTOS.startScheduler(); } 

И опять проверка возможна только под отладчиком, поскольку выводить информацию пока что некуда. И вот загружаем ставим точку остановки на задаче переодического расчета температуры, жмем раз 40 F5(Run), чтобы фильтр устаканился и смотрим на значение температуры — выделно красным 23.68 С, ну по ощущениям так есть, ну возможно 23.62 С 🙂

image

Сохраняем проект, чтобы не забыть: Кнопки, Светодиоды, Температура на IAR 6.50
Тот же самый фокус повторяем с переменными Vdda и Trimmer (переменный резистор). Архитектура идентичная архитектуре класса cTemeperature.

image

А сам контейнер перменных и по совместительству активный объект — cVariableDirector стал выглядеть вот так:

image

Добавляем вызов расчета напряжений в cVariableDirector

VariableDirector.cpp

#include "variablesdirector.h"  // описание класса  #include "frtoswrapper.h"       // для oRTOS #include "susuassert.h"         // для ASSERT #define VARIABLESDIRECTOR_DELAY (tU32)40/portTICK_PERIOD_MS /******************************************************************************* * Function:  constructor * Description: включает АЦП ******************************************************************************/ cVariablesDirector::cVariablesDirector(cAdcDirector* pAdcDirector) {   ASSERT(pAdcDirector != NULL);   this->pTemperature = new cTemperature(pAdcDirector);   this->pVdda =  new cVdda(pAdcDirector);   this->pTrimmer =  new cTrimmer(pAdcDirector); } /******************************************************************************* * Function:  run * Description: Задача  расчета температуры ******************************************************************************/ void cVariablesDirector::run(void) {   for(;;)   {     this->pTemperature->calculate();     this->pVdda->calculate();     this->pTrimmer->calculate();     oRTOS.taskDelay(VARIABLESDIRECTOR_DELAY);   } } 

Запускаем на отладку и получаем следующий результат(красные циферки атрибутов value): Как видно температура 23.5С, Vdda как и написано в документации 2.72, а напряжение на потенциометре 2.52 (но его можно менять, повервнут ролик резистора)

image

Ну вот и реализовано основное требование проекта, надо признаться провозился я с ним дольше запланированного — почти 7 дней. Но это больше из-за непоняток с напряжением переменного резистора, очень долго тупил, почему не измеряется ничего. Хорошо додумался подсмотреть у разработчиков платы Olimex 🙂 Осталась одна небольшая задача — вывод на индикатор. Я подумал, что ничего сложного не будет, поскольку тот мой последний проект 8-летней давности как раз был на PIC16 со встроенным драйвером индикатора, и уж индикатор то мне дастся очень просто. А как это получилось, я расскажу в заключительной части.
Да забыл совсем — сохранил же проект тут:
Кнопки, Cветодиоды, и все переменные на IAR 6.50

ссылка на оригинал статьи http://habrahabr.ru/post/261823/


Комментарии

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

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