DMA (ПДП) и Таймер в STM32F103C8T6. Подключаем DHT11 (DHT22).

DMA (ПДП) и Таймер в STM32F103C8T6. Подключаем DHT11 (DHT22).

Я уже рассказывал, как можно подключить DHT11 или DHT22 к STM32 с программной реализацией протокола. Теперь хотел бы поделится способом, где часть работы переложена на DMA и таймер

Программная реализация описана в статье Подключение DHT11 и DHT22 к STM32F103C8T6.

Обмен данными STM32F103 с DHT11(DHT22).

Более подробно описано в статье по ссылке выше, здесь продублирую для удобства некоторую информацию.

DHT11 DTH22 распиновкаDHT11 DHT22 схема подключения

dht11 dht22 data protoloc

Датчик питается примерно от 3.3 до 5.5В. Линия данных (2 пин датчика) подтягивается к питанию.

Инициализация датчика DHT11 более 18 ms, для датчика DHT22 более 1 ms.
Датчик передает сигнал присутствия прижимая линию к 0 на 80 us и отпуская на 80 us.
Затем следуют 40 бит от старшего к младшему. Каждый бит начинается с прижатия линии к 0 на 50 us. Засекам этот промежуток и сравниваем с последующим промежутком, в котором линия будет поднята к питанию. Если линия поднята к питанию на время большее чем была прижата к 0 - значит мы получили 1, если меньше - значит пришел 0.

Что такое DMA в STM32?

Главной задачей DMA (Direct Memory Access), или в русском ПДП (Прямой Доступ к Памяти), является обмен информацией на аппаратном уровне между памятью мк и периферией.

DMA принимает указатель на память в мк (регистр CMARx) и на адрес регистра периферии (регистр CPARx). С помощью регистров  CNDTR и CCR можно указать сколько данных хранится в памяти, размер этих данных, нужно ли обходить их смещая указатель, нужно ли обходить данные по кругу, а так же направление передачи данных от периферии или в нее.

Более подробно опишу в коде, и в отдельной статье.

Опрос датчика DHT11(DHT22) с DMA, PWM и TIMx.

Нога подтянута к питанию. Затем прижимаем ее к земле для инициализации на нужное время и отпускаем.

Настроим таймер на 500 ms. С помощью PWM (ШИМ) будем прижимать ногу к питанию, а передавать скважность PWM будет с помощью DMA. Затем замеряем с помощью таймера промежутки между спадами питания. Складируем с помощью DMA в массив. В конце обрабатываем наш массив и получаем данные температуры и влажности.

У STM32F103 есть 1 DMA.

Для подключения датчика я выбрал порт PB6, так как он толерантен к 5В и имеет привязку к таймеру и DMA (ПДП). Я использую резистор в 10К для подтяжки линии данных к питанию.

 Summary of DMA1 requests for each channel

Нам нужны DMA Channel 7, который будет брать данные из массива, в котором будет скважность ШИМ, и отправлять в таймер. И Channel 4, который будет слушать второй канал TIM4 и складывать замеры в массив.

Дальше буду пояснять в комментариях к коду. 

#define DHT_TIM_EN RCC_APB1ENR_TIM4EN
#define DHT_TIM TIM4
#define DHT_TIM_IRQ TIM4_IRQHandler
#define DHT_DMA DMA1
#define DHT_DMA_CH_PWM DMA1_Channel7
#define DHT_DMA_CH_IN DMA1_Channel4
#define DHT_DMA_IRQ DMA1_Channel4_IRQHandler
// включить тактирование DMA
#define DHT_DMA_ENABLE() RCC->AHBENR |= RCC_AHBENR_DMA1EN
// выключить тактирование DMA
#define DHT_DMA_DISABLE() RCC->AHBENR &= ~RCC_AHBENR_DMA1EN
// проверить включено ли тактирование
// ниже поясню
#define DHT_DMA_ENABLED() ((RCC->AHBENR & RCC_AHBENR_DMA1EN) != 0)
#define DHT_TIM_ENABLE() RCC->APB1ENR |= DHT_TIM_EN
#define DHT_ENABLE_IRQ() NVIC_EnableIRQ(TIM4_IRQn);NVIC_EnableIRQ(DMA1_Channel4_IRQn)

// размер массива для установки в CCR1
#define DHT_DELAY_SIZE 4
// размер буфера измерений канала 2
#define DHT_BUF_SIZE 42

uint16_t DHTTimDelay[DHT_DELAY_SIZE] = { 0xffff, 48199, 0xffff, 0xffff };
uint16_t DHTBuff[DHT_BUF_SIZE] = { 0 };

typedef struct {
    uint8_t HI;
    uint8_t HD;
    uint8_t TI;
    uint8_t TD;
    uint8_t crc;
} temp_s;
 
temp_s data;
 
#define DHT_SIZE 5 // size of temp_s
#define DHT_GPIO GPIOB
#define DHT_PIN GPIO_Pin_6

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

Бит у нас 40, но размер буфера 42, так как мы измеряем промежутки между заваливанием фронтов, то туда попадет еще и сигнал присутствия.

Число 48200 поясню позже.

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

void initGPIO() {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
 
    GPIO_InitTypeDef port;
    GPIO_StructInit(&port);
 
    port.GPIO_Mode = GPIO_Mode_AF_OD;
    port.GPIO_Pin = DHT_PIN;
    port.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DHT_GPIO, &port);

Дальше настройка таймера, DMA и обработчики прерываний

void DHT_DMA_Init(void) {
    DHT_DMA_CH_PWM->CCR = 0;
    // задаем адрес в памяти мк
    // отсюда DMA будет брать данные и передавать в таймер
    DHT_DMA_CH_PWM->CMAR = &DHTTimDelay[0];
    // указатель на регистр периферии
    // сюда DMA будет писать данные взятые из памяти
    DHT_DMA_CH_PWM->CPAR = &DHT_TIM->CCR1;
    // размер массива, для того что бы DMA знал сколько раз он может перемещаться по массиву
    DHT_DMA_CH_PWM->CNDTR = DHT_DELAY_SIZE;
    // PL - устанавливаем самый высокий приоритет канала
    // MSIZE и PSIZE - указываем что размер в памяти и периферии данных 16 бит соответсвенно
    // MINC - говорит что по данным нужно перемещаться
    // CIRC - Сircular режим, то есть по достижении концы данных сбросить счетчик и начать сначала
    // DIR - направление передачи данных, из памяти в периферию
    DHT_DMA_CH_PWM->CCR = DMA_CCR7_PL | DMA_CCR7_MSIZE_0 | DMA_CCR7_PSIZE_0 | DMA_CCR7_MINC | DMA_CCR7_CIRC | DMA_CCR7_DIR;
    DHT_DMA->IFCR = DMA_IFCR_CGIF7 | DMA_IFCR_CHTIF7 | DMA_IFCR_CTCIF7 | DMA_IFCR_CTEIF7;
    // включаем канал DMA
    DHT_DMA_CH_PWM->CCR |= DMA_CCR1_EN;
 
    DHT_DMA_CH_IN->CCR = 0;
    // задаем адрес куда писать данные из таймера
    DHT_DMA_CH_IN->CMAR = &DHTBuff[0];
    // откуда их брать
    DHT_DMA_CH_IN->CPAR = &DHT_TIM->CCR2;
    // сколько раз получить данные до того как сработает прерывание
    DHT_DMA_CH_IN->CNDTR = DHT_BUF_SIZE;
    // разрешить прерывание по достижении конца
    DHT_DMA_CH_IN->CCR = DMA_CCR4_PL | DMA_CCR4_MSIZE_0 | DMA_CCR4_PSIZE_0 | DMA_CCR4_MINC | DMA_CCR4_TCIE;
    DHT_DMA->IFCR = DMA_IFCR_CGIF4 | DMA_IFCR_CHTIF4 | DMA_IFCR_CTCIF4 | DMA_IFCR_CTEIF4;
}
 
void DHT_TIM_Init(void)
{
    // включаем тактирование DMA и таймера
    DHT_DMA_ENABLE();
    DHT_TIM_ENABLE();

    // инициализируем DMA каналы
    DHT_DMA_Init();

    // поделим нашу частоту на 720
    // 71 999 999 / 720 примерно 99 999.999 тиков в секунду
    DHT_TIM->PSC = 720;
    // настроим до куда считать
    DHT_TIM->ARR = 49999;
    DHT_TIM->SR = 0;
    // включаем DMA на канале 2, разрешаем прерывание на 1 канале таймера
    DHT_TIM->DIER = TIM_DIER_UDE | TIM_DIER_CC1IE | TIM_DIER_CC2DE;
    // включаем режим PWM 2 и настраиваем что бы
    // данные приходящие на CH1 переходили на IC2
    DHT_TIM->CCMR1 = TIM_CCMR1_OC1M | TIM_CCMR1_CC2S_1;
    DHT_TIM->CR1 = TIM_CR1_ARPE;
    DHT_TIM->EGR = TIM_EGR_UG;
    // указываем, что активным состоянием вывода таймера является 0 (земля)
    // и включаем ножку мк на работу с таймером
    // включаем ногу на захват для канала 2 и захват по спадению фронта
    DHT_TIM->CCER = TIM_CCER_CC1E | TIM_CCER_CC1P | TIM_CCER_CC2E | TIM_CCER_CC2P;
    DHT_TIM->CR1 |= TIM_CR1_CEN;
 
    DHT_ENABLE_IRQ();
}
 
void DHT_DMA_IRQ(void)
{
    if(DHT_DMA->ISR & DMA_ISR_TCIF4)
    {
        DHT_DMA->IFCR = DMA_IFCR_CTCIF4;
        // выключаем DMA
        DHT_DMA_DISABLE();
    }
}
 
 
void DHT_TIM_IRQ(void)
{
    if(DHT_TIM->SR & TIM_SR_CC1IF)
    {
        // при переполнении таймера перезапускаем слушающий DMA канал
        DHT_DMA_CH_IN->CCR &= ~DMA_CCR4_EN;
        DHT_DMA_CH_IN->CMAR = &DHTBuff[0];
        DHT_DMA_CH_IN->CNDTR = DHT_BUF_SIZE;
        DHT_DMA_CH_IN->CCR |= DMA_CCR4_EN;
 
        DHT_TIM->SR &= ~TIM_SR_CC1IF;
    }
}

Немного поясню работу.

У нас есть два канала таймера:
1 канал нужен что бы прижать линию данных к 0 для инициализации
2 канал считает когда приходит очередной бит

PWM у нас в режиме 2, то есть канал 1 неактивен пока CCR1 больше чем счетчик таймера. Активный канал прижимает линию к 0.

Сначала DMA пишет в CCR1 = 0xffff - то есть CCR1 всегда больше, а значит всегда активен, а значит на выходе всегда 1. Это нужно чтобы на датчик какое то время подавалось питание.

Затем DMA запишет новое значение в канал 1 таймера. Это будет 48199, таймер настроен на 49999. То есть пока счет будет меньше чем 48199 на линии продолжит оставаться 1. А в оставшиеся 49999 - 48199 = 1800 тиков таймера будет 0. То есть сигнал старт пойдет примерно через 500 us + 482 us, что хватает для вычисление температуры датчиком.

Режим PWM 2 и я выбрал чтобы прижатие к 0 было в конце, и сразу после него был сброс DMA канала 2, и начать принимать данные в массив с начала нового цикла таймера.

С учетом нашего предделителя в секунду у нас выполняется примерно 100 000 тиков, то есть за 1 ms проходит 100 тиков. 1800 / 100 = 18 ms. Именно по этому я поставил такое число. То есть это команда старта от мк к датчику DHT11.

Дальше DMA передаст число 0xffff, а значит таймер отпустит ногу от 0. В это же время, от датчика пойдет сигнал присутствия на линии и передача бит. Тут наш канал 2 начинает записывать в DTHBuf поочередно промежутки когда был переход в 0.

Первый переход в 0 будет от сигнала присутствия. Последний, по окончании передачи последнего бита. Итого 1 + 40 перед каждым битом + 1 в конце = 42. По этому массив у нас не из 40 байт а из 42.

Каждые 500 ms таймер сбрасывается. В прерывании повторно инициализируем 2 канал DMA.

По даташиту не рекомендуют опрашивать таймер чаще 1-го раза в 2 секунды. У меня на 1 опрос уходит 4 цикла работы таймера * 500 us.

В прерывании не желательно выполнять длительный код, по этому я выключаю DMA после того как данные получены, а в цикле приложения я проверяю, если DMA выключен - значит пришли данные. Привожу данные к нашей структуре и вывожу на экран, после того как данные приведены, можно запускать еще раз таймер.

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

перевести данные из массива в структуру можно как то так

uint8_t DHTCalc(temp_s *data){
  uint8_t cnt, i, b, k, n = 0;
  uint8_t* buf = data;
 
  for (b = 0; b < DHT_SIZE; ++b) {
     *buf = 0;
     for (i = 0; i < 8; ++i) {
         k = (b << 3) + i + 1;
         cnt = DHTBuff[k + 1] - DHTBuff[k];
        *buf |= (cnt > 10) << (7 - i);
     }
     buf++;
  }
 
  if (data->crc != data->HD + data->HI + data->TD + data->TI) return ERROR;
 
  return SUCCESS;
}

а опрашивать так

DHT_TIM_Init();
uint8_t status = 0;
while (1) {
  // если DMA выключен, значит данные есть
  if (!DHT_DMA_ENABLED()) {
    // читаем пока выключен DMA
    status = DHTCalc(&data);
    // включаем DMA
    DHT_DMA_ENABLE();
    if (status) {
      // выводим данные
      ClearLCDScreen();
      snprintf(buf, 20, "Temp: %d.%d", data.TI, data.TD);
      PrintStr(buf);
      Cursor(1, 0);
      snprintf(buf, 20, "H: %d.%d", data.HI, data.HD);
      PrintStr(buf);
    } else {
      // что то пошло не так
    }
  }
}

Для общего понимания возможностей DMA должно хватить.

При копировании материалов ссылка на https://terraideas.ru/ обязательна

Комментарии к статье: DMA (ПДП) и Таймер в STM32F103C8T6. Подключаем DHT11 (DHT22).

Александр 10 месяцев назад

Спасибо! Очень помогли. Статьи четкие, содержательные, понятные. Буду заходить в надежде увидеть новые.... (например Модбасом :-) )