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

  • Tutorial

Введение


В прошлой публикации 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
******************************************************************************/
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;
}



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;       
}



Тот же самый фокус повторяем с переменными Vdda и Trimmer (переменный резистор). Архитектура идентичная архитектуре класса cTemeperature.

Ну вот и реализовано основное требование проекта, надо признаться провозился я с ним дольше запланированного — почти 7 дней. Но это больше из-за непоняток с напряжением переменного резистора, очень долго тупил, почему не измеряется ничего. Хорошо додумался подсмотреть у разработчиков платы Olimex :) Осталась одна небольшая задача — вывод на индикатор. Я подумал, что ничего сложного не будет, поскольку тот мой последний проект 8-летней давности как раз был на PIC16 со встроенным драйвером индикатора, и уж индикатор то мне дастся очень просто. А как это получилось, я расскажу в заключительной части.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 12

    0
    void cVariablesDetector::run(void)
    


    Зачем второй void? В смысле, для чего он в Си — я знаю, но в C++ он для этого не нужен. Особенности IAR?
      –2
      Да, вы правы, в С++ в нем смысла нет: (void) и () — это одно и то же. Но так написано в стандарте кодирования. А в стандарте написано, потому что видимо раньше писали на Си, и перетекло оттуда.
      А еще lint ругается — но сам же пишет, что в С++ это означает тоже самое что с void, что без void. В общем, чтобы удовлетворить lint и пройти без замечаний.
      1717 empty prototype for function declaration, assumed '(void)' — An empty prototype,
      as in:
      void f();
      has a different meaning in C than in C++. In C it says nothing about the arguments of the function;
      in C++, it says there are no arguments. This message is not given for member function
      declarations or for function definitions. Rather, weaker Elective Notes (1917 and 1918) are
      given. This is because the chance of ambiguity does not exist in these cases. [11 section 8.2.5]
        +2
        Да точно, еще раз вглянул стандарт С++
        The parameter-declaration-clause determines the arguments that can be specified, and their processing,
        when the function is called. [Note: the parameter-declaration-clause is used to convert the arguments
        specified on the function call; see 5.2.2. ] If the parameter-declaration-clause is empty, the function takes
        no arguments. The parameter list (void) is equivalent to the empty parameter list.
          0
          Я не отрицал выделенного. Вопрос про избыточность был. Но вообще:
          Но так написано в стандарте кодирования.

          хватило бы: от них иногда плеваться хочется, но никуда не попрёшь. Я, правда, думал, что вы для себя пишите. А откуда взялись tU32, tF32, tBoolean?
            –2
            Ну это для себя было, точнее в свободное время для курса в университете(на волонтерских началах пока что), но чисто дома. К стандарту привык, потому что когда ревью кода делаю(сам писал давно очень, а ревью делаю переодически), моя задача проверить код на стандарт первым делом. Типы тоже указаны в стандарте.Скорее всего я его отдельно очень упрощенный для университета сделаю.
        +1
        Наследование от iFilter выглядит плохо. Да и не интерфейс это, раз вы по iFilter* ни к кому не обращаетесь. Лучше создайте фильтр отдельно и заагрегируйте как-нибудь.
          0
          Да — согласен.
          0
          Не понимаю, зачем вы выкидываете всю настройку железа в __low_level_init(). Получается недо ооп. Если поменять ацп с ADC1 на ADC2, нужно и менять и функцию инициализации и сам класс.
          Логично инициализировать периферию в конструкторе класса ацп, ведь он и предназначался для связи логики с железом.

          При этом отдельно хочется заметить, что динамическое выделение памяти под классы, относящиеся к железу, во всраиваемых системах это моветон. Мало где увидишь плату, где например припаян разъем usb, а через секунду эти же ножки заняты внешней припаянной flash памятью или lcd дисплеем.
          Как уже где-то писалось, крайне не хочется во время нажатия на педаль тормоза в автомобиле получить exception о том, что недостаточно памяти для выделения памяти под класс cBrakePushHandler.
            0
            Да по поводу динамического выделения памяти — согласен, в 3 части как раз фильтр реализовал, без него.
            По поводу создания объектов с main(), тут есть особенность freeRTOS, если объявлять в main() вот так:
              0
              Извиняюсь нажал на Enter случайно…
              1. Да по поводу динамического выделения памяти — согласен, в 3 части как раз фильтр реализовал, без него.
              По поводу создания объектов с main(), тут есть особенность freeRTOS, если объявлять в main() вот так:
              void main (void)
              {
                cLedsDirector oLedsDirector;
                ...
                oRTOS.creatTask(&oLedsDirector...) ;
                oRTOS.startScheduler();
               
              }
              

              То вообще из-за особенностей работы freeRTOS, ничего работать не будет, посколько oLedsDirector создан на стеке, а при запуске планировщика и первой задачи, весь стек идет к чертям собачим, так как freeRTOS указатель стека перезаписывает на начало.
              Поэтому тут надо будет либо немного «патчить» операционку, либо создавать объект oLedsDirector глобально. А глобальные объекты, тоже как то не по понятиям.
              Поэтому нью тут решение подходящее, можно его переопределить, если уж совсем надо.
              Из-за этой особенности операционки, нельзя создавать локальные переменные в мейне, ну точнее можно, но надо знать, что их потом нельзя использовать.
              Ну и в данном проекте, все объекты создаются один раз в одном месте. и живут пока ресет не нажмут. Поэтому тут ничего плохого в этом нет. Никакого переполнения памяти не будет.

              2. В лоу левел инит, скинул, только, то что не надо для конкретного проекта перенастраивать никогда. Но конечно, же это просто так мне захотелось, можно сделать и настройку отдельно, но я не хотел вообще железо(которое не перенастраивается) нигде трогать — настроил и забыл, а в классы вынес только, то что по ходу меняется. Там в регистр записать что-то, проверить и так далее…
                0
                Про переопределение стека фриртосом спасибо, не знал.
                По поводу C++ в embedded советую книгу Real time c++
                  0
                  Спасибо за книгу, обязательно почитаю.

            Only users with full accounts can post comments. Log in, please.