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       // Регистры 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           // Регистры 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

© Habrahabr.ru