DMA in the IDE, Part IV

STM32 DMA ADC P2M Demo

This is the fourth example of DMA usage on the STM32F405 Feather board. Programming is from the Arduino IDE. This example demonstrates the peripheral to memory (P2M) DMA Mode. We send data from the ADC peripheral to memory via DMA.

This code sends the internal temperature of the MCU to the serial monitor every second.

The Code

// DMA ADC P2M Demo.
// Adafruit SMT32F405 Feather.
//
// stm32f405 CubeMX Setup:
// ADC1, Temperature Sensor Channel.
// Continuous Conversion Mode: Enabled.
// DMA Continuous Requests: Enabled.
// Number of Conversion: 16.
// DMA Request, Add: ADC1
// [per stm32f405 Ref Man, pg 308, DMA2 Request Mapping Table]
// System Core, DMA2 Request, Channel 0, ADC1 Stream4.
// Increment memory by half-word (12-bit resolution).
//
// Tools > C Runtime Library: Newlib Nano + Float Scanf
//
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

volatile bool adcDataAvailable __attribute__ ((aligned));
volatile uint16_t adcData[16];
constexpr uint32_t INTERVAL = 1000; // ms.
uint32_t t = millis();

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

// Handle DMA2 stream4 global interrupt.
extern "C" void DMA2_Stream0_IRQHandler(void) {
  HAL_DMA_IRQHandler(&hdma_adc1);
}

// ADC conversion complete callback.
extern "C" void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { 
  UNUSED(hadc); 
  adcDataAvailable = true;
}

void adcInit(void) {
  ADC_ChannelConfTypeDef sConfig = {0};

  __HAL_RCC_ADC1_CLK_ENABLE();
  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV2;
  hadc1.Init.Resolution = ADC_RESOLUTION_12B;
  hadc1.Init.ScanConvMode = DISABLE;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.NbrOfConversion = 16;
  hadc1.Init.DMAContinuousRequests = ENABLE;
  hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
    while(1);
  
  hdma_adc1.Instance = DMA2_Stream0;
  hdma_adc1.Init.Channel = DMA_CHANNEL_0;
  hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
  hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
  hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
  hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
  hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
  hdma_adc1.Init.Mode = DMA_NORMAL;
  hdma_adc1.Init.Priority = DMA_PRIORITY_LOW;
  hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
  if (HAL_DMA_Init(&hdma_adc1) != HAL_OK)
    while(1);

  __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

  sConfig.Channel = ADC_CHANNEL_TEMPSENSOR;
  sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
  for (uint32_t i=1; i<=16; i++) {
    sConfig.Rank = i;
    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
      while(1);
  }
}

void dmaInit(void) {
  __HAL_RCC_DMA2_CLK_ENABLE();
  HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}

// Convert sample to temperature.
float calcTemp() {
  // See STM32F405 Reference Manual pg. 413.
  const float AVG_SLOPE = 2.5;
  const float V25 = 0.76;
  const float ADC_TO_VOLT = 3.3 / (4096 - 1);
  float adcValue = 0.0;
 
  for (int i=0; i<16; i++) {
    adcValue += (float)adcData[i];
    adcData[i] = 0;
  }
  adcValue /= 16.0;
  
  float vSense = adcValue * ADC_TO_VOLT;
  float temp = (vSense - V25) / AVG_SLOPE + 25.0f;
  return temp;
}

void setup() {
  dmaInit();
  adcInit();
  Serial.begin(9600);
  while(!Serial);
  adcDataAvailable = false;
}

void loop(void) {
  if (millis() > t) {
    t = millis() + INTERVAL;
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcData, 16);
  }
  
  if (adcDataAvailable) {
    char s[16] = {0};
     
    adcDataAvailable = false;
    sprintf(s, "temp = %3.1f C", calcTemp());
    Serial.println(s);
  }
}
Posted in Uncategorized | Leave a comment

DMA in the IDE, Part III

STM32 DMA UART Transmit M2P Demo

This is the third example of DMA usage on the STM32F405 Feather board. Programming is from the Arduino IDE. This example demonstrates the memory to peripheral (M2P) DMA Mode. We send data from memory to the UART via DMA.

For this demo to work, the Arduino serial code needs to be disabled. The instructions are included in the code comments. Also, you will note some code inside the main loop function. The purpose of this code is to reset the transmit registers so the MCU acknowledges the transmission is complete. This code would not be necessary except the Arduino STM32 code uses the USART1_IRQHandler and therefore doesn’t reset after the the UART1 transmit.

Make a serial connection to the SCL pin (PB6). Every press of the button send a paragraph of text to the serial terminal.

The Code

// DMA USART2 transfer M2P memory to peripheral.
// Adafruit STM32F405 Feather.
//
// Add build_opt.h file with -DHAL_UART_MODULE_ENABLED
// Tools > U(S)ART Support: Disabled.
//
// SCL (PB6) TX pin.
// 10 (PB9) Button pin.
//

uint8_t data_stream[] =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure \
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \
mollit anim id est laborum.\r\n";

uint32_t data_length{sizeof(data_stream)};

uint32_t deBounce{0};
uint32_t lastBounce{0};
constexpr uint32_t DEBOUNCE_INTERVAL{50};

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;

extern "C" void DMA2_Stream7_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_usart1_tx); }

void uartInit(void) {
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 9600;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  
  __HAL_RCC_USART1_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  
  // USART1 GPIO Configuration    
  // PB6 --> USART1_TX
  // PB7 --> USART1_RX 
  GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  hdma_usart1_tx.Instance = DMA2_Stream7;
  hdma_usart1_tx.Init.Channel = DMA_CHANNEL_4;
  hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
  hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
  hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
  hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
  hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
  hdma_usart1_tx.Init.Mode = DMA_NORMAL;
  hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
  hdma_usart1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
  if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
      while(1);
      
  __HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);

  if (HAL_UART_Init(&huart1) != HAL_OK)
    while(1);
}

void dmaInit(void) {
  __HAL_RCC_DMA2_CLK_ENABLE();
  HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn);
}

void buttonISR() {
  if (millis() > deBounce) {
    deBounce = millis() + DEBOUNCE_INTERVAL;
    HAL_UART_Transmit_DMA(&huart1, data_stream, (uint16_t)data_length);
  }
}

void setup(void) {
  dmaInit();
  uartInit();
  pinMode(10, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PB9), buttonISR, FALLING);
}

void loop() {
  if (deBounce != lastBounce) {
    lastBounce = deBounce;
    // UART in mode Transmision end?
    while ( !(huart1.Instance->SR & USART_SR_TC) && 
            !(huart1.Instance->CR1 & USART_CR1_TCIE) ) ;
    // Disable TXEIE and TCIE interrupts. 
    huart1.Instance->CR1 &= ~(USART_CR1_TXEIE | USART_CR1_TCIE);
    // At end of Tx process, restore huart->gState to ready. 
    huart1.gState = HAL_UART_STATE_READY;
 } 
}

Posted in Uncategorized | Leave a comment

DMA in the IDE, Part II

STM32 DMA UART Receive P2M Demo

This is the second example of DMA usage on the STM32F405 Feather board. Programming is from the Arduino IDE. This example demonstrates the peripheral to memory (P2M) DMA Mode. We send serial data to the board via UART DMA storing the data into SRAM memory.

For this demo to work, the Arduino serial code needs to be disabled. The instructions are included in the code comments.

Make a serial connection to the SDA pin (PB7). Every 5 characters the board receives toggles the onboard LED.

The Code

// USART1 Rx to SRAM DMA transfer.
// Interrupt mode, transfer complete initiates a callback.
//
// Add build_opt.h file to project with -DHAL_UART_MODULE_ENABLED.
// Tools > U(S)ART Support: Disabled.
// Change option in file: stm32f4xx_hal_conf_default.h
// Located here: 
// C:\Users\default.LAPTOP-7V09ROBA\AppData\Local\Arduino15\packages\STM32\hardware\stm32\1.9.0\system\STM32F4xx
// #define USE_HAL_UART_REGISTER_CALLBACKS 1U
//
// Use TeraTerm to connect to board at 9600 baud.
// USART1 RX is on SDA pin, PB7.
// Every 5 chars sent should toggle LED.
//
#define LED_Pin       GPIO_PIN_1
#define LED_GPIO_Port GPIOC

#define DATA_LENGTH 5
uint8_t data[DATA_LENGTH];

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;

extern "C" void myCallback(UART_HandleTypeDef *huart) {
  UNUSED(huart);
  HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}

extern "C" void DMA2_Stream2_IRQHandler(void) {
  HAL_DMA_IRQHandler(&hdma_usart1_rx);
}

void uartInit(void) {
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 9600;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;

  __HAL_RCC_USART1_CLK_ENABLE();
  hdma_usart1_rx.Instance = DMA2_Stream2;
  hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
  hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
  hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
  hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
  hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
  hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
  hdma_usart1_rx.Init.Mode = DMA_NORMAL;
  hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
  hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
  if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
    while(1);

  __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);

  if (HAL_UART_Init(&huart1) != HAL_OK)
  while(1);
}

void dmaInit(void) {
  __HAL_RCC_DMA2_CLK_ENABLE();
  HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);
}

void gpioInit(void) {
  GPIO_InitTypeDef GPIO_InitStruct;

  __HAL_RCC_GPIOB_CLK_ENABLE();

  // USART1 GPIO Configuration.
  // PB6 --> USART1_TX
  // PB7 --> USART1_RX 
  GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

void setup() { 
  gpioInit();
  dmaInit();
  uartInit();
  pinMode(LED_BUILTIN, OUTPUT);
  HAL_UART_RegisterCallback(&huart1, HAL_UART_RX_COMPLETE_CB_ID, (pUART_CallbackTypeDef)myCallback);
}

void loop() {
  __HAL_UART_FLUSH_DRREGISTER(&huart1);

  while (1)
    HAL_UART_Receive_DMA(&huart1, (uint8_t *)data, DATA_LENGTH);
}
Posted in Uncategorized | Leave a comment

DMA in the IDE

Blinking an LED with STM32 DMA M2M Mode

The following program is for the Adafruit STM32F405 Feather board. It is a simple demonstration of using DMA to blink the onboard LED. DMA seemed very mysterious and complicated to me, until I completed this excellent uDemy online course.

This, and the following four posts (DMA UART Receive, DMA UART Transmit, DMA ADC, and DMA Register-Level Programming) will present the exercises from the course, as I have adapted them to the Arduino IDE and the STM32F405 Feather board.

However, you really should take the course to get all of the theory and details I will be skipping over. The original source code for the online course exercises is located here.

STM32 DMA

Direct Memory Access (DMA) is a component of the microcontroller that can be used in conjunction with the main microprocessor in order to offload memory transfer operations. This significantly reduces the CPU load. The DMA controller can perform memory to memory data transfers as well as peripheral to memory data transfers or vice versa. The existence of DMA with a CPU can accelerate its throughput by orders of magnitude. Data can be quickly moved by DMA without any CPU actions. This keeps CPU resources free for other operations.

The STM32F405 MCU has two DMA controllers.

Each DMA transfer consists of three operations:

  1. Loading of data from the peripheral data register or a location in memory addressed through a peripheral/memory address register.
  2. Storage of the data loaded to the peripheral data register or a location in memory.
  3. Post-decrementing of the DMA counter, which contains the number of transactions that still need to be performed.

DMA Memory-To-Memory

The DMA can also work without a request from a peripheral. This mode is called Memory to Memory mode.

DMA Interrupts

An interrupt can be produced on a Half-transfer, Transfer complete, or Transfer error for each DMA channel. Separate interrupt enable bits are available for flexibility.

The easiest method to setup the registers for a DMA operation is by using the CubeMX software and then transferring the required portions of code to the Arduino IDE. The HAL APIs which configure the DMA units and programmatically set the buffer lengths, DMA source, destination, and required details are fully usable inside the IDE.

The only issue is to properly integrate the DMA code with the existing STM32 Arduino software. Many peripheral items are used by the Arduino.

The Code

// Use DMA M2M Mode to blink LED.
//
// STM32F405 Feather, toggle ODR for GPIOC, Pin 1 on AHB1 bus.
//
// To enable setting callback, change the following option: 
//   #define USE_HAL_UART_REGISTER_CALLBACKS 1U
// In the following file:
//   stm32f4xx_hal_conf_default.h
// File is located here: 
//   C:\Users\USER\AppData\Local\Arduino15\packages\STM32\hardware\stm32\1.9.0\system\STM32F4xx
// The callback is not used in this example.

DMA_HandleTypeDef hDMA2_Stream0;

// DMA IRQ handler
extern "C" void DMA2_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(&hDMA2_Stream0); }

// DMA transfer complete callback.
extern "C" void dmaXferCompleteCallback(DMA_HandleTypeDef *pHandle) { UNUSED(pHandle); }

static void dmaInit(void) {
  __HAL_RCC_DMA2_CLK_ENABLE();

  hDMA2_Stream0.Instance = DMA2_Stream0;
  hDMA2_Stream0.Init.Channel = DMA_CHANNEL_0;
  hDMA2_Stream0.Init.Direction = DMA_MEMORY_TO_MEMORY;
  hDMA2_Stream0.Init.PeriphInc = DMA_PINC_DISABLE;
  hDMA2_Stream0.Init.MemInc = DMA_MINC_DISABLE;
  hDMA2_Stream0.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
  hDMA2_Stream0.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
  hDMA2_Stream0.Init.Mode = DMA_NORMAL;
  hDMA2_Stream0.Init.Priority = DMA_PRIORITY_LOW;
  hDMA2_Stream0.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
  hDMA2_Stream0.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
  hDMA2_Stream0.Init.MemBurst = DMA_MBURST_SINGLE;
  hDMA2_Stream0.Init.PeriphBurst = DMA_PBURST_SINGLE;
  if (HAL_DMA_Init(&hDMA2_Stream0) != HAL_OK)
    while(1);

  HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}

void setup() {
  pinMode(PC1, OUTPUT);
  dmaInit();
}

void loop() {
  //HAL_DMA_RegisterCallback(&hDMA2_Stream0, HAL_DMA_XFER_CPLT_CB_ID, &dmaXferCompleteCallback);

  while (1)   {
    uint8_t ledPin[2] = { 0b00000010, 0x00 }; // GPIOC, pin 1.
    
    HAL_DMA_Start_IT(&hDMA2_Stream0, (uint32_t)&ledPin[0], (uint32_t)&GPIOC->ODR, 1);
    delay(1000);
    HAL_DMA_Start_IT(&hDMA2_Stream0, (uint32_t)&ledPin[1], (uint32_t)&GPIOC->ODR, 1);
    delay(1000);
  }
}

Posted in Uncategorized | Leave a comment

Adafruit STM32F411 BlackPill and STM32F405 Feather Development Boards

STM32F405 Feather Description

The STM32F405 Feather that Adafruit designed runs at 168MHz. It has a STEMMA QT/Qwiic port on it so you can easily plug and play I2C sensors. It is usable with the Arduino IDE or STM32Cube IDE. This Feather board has lots of features:

  • STM32F405 Cortex M4 with FPU and 1MB Flash, 168MHz speed
  • 192KB RAM total – 128 KB RAM for general usage + 64 KB program-only/cache RAM
  • 3.3V logic, but almost all pins are 5V compliant!
  • USB C power and data – our first USB C Feather!
  • LiPo connector and charger
  • SD socket on the bottom, connected to SDIO port
  • 2 MB SPI Flash chip
  • Built in NeoPixel indicator
  • I2C, UART, GPIO, ADCs, DACs
  • Qwiic/STEMMA-QT connector for fast I2C connectivity
  • Built-in USB DFU bootloader to load firmware.

Arduino is supported through STM32duino.There’s no auto-reset support, so you have to pull the BOOT0 pin high and manually reset before uploading.

One of the shortcomings of the vey small feather packaging is that several of the pins were not brought out to connectors. Specifically, PA0, PA1, PA2, PA3, PA8, PA9, PA10, PA0, PA1 and PC13 have been excluded. Additionally, the SD card and SPI Flash consume a total of 11 other pins. So, pin shortage could be a really issue with this board.

Blackpill Description

This simple development board features an STM32F411CEU6 chip with 512 KB of flash, 128 KB of SRAM, and runs at 100 MHz. The board features a USB C connector, with a 3.3V 100mA LDO regulator. There are both 25mhz and 32.768 KHz crystals. There are a few handy buttons: a BOOT button for entering the ROM DFU bootloader, a reset button, and a generic button on PA0 for users. One power LED and one user-controllable LED on PC13. The STM32F411 chip itself has multiple UART, I2C, SPI, I2S, and timer peripherals (datasheet). The USB is full speed. There’s a single ADC multiplexed to 10 inputs. There is also a spot on the bottom for SOIC flash memory chip, like a GD25Q16 – 2MB SPI Flash.

Again, thanks to the STM bootloader in ROM, you don’t need a programmer to load binary firmware, and you can use STM32duino for Arduino IDE support.

Simple Neopixel Demonstration on the STM32F405 Feather

// Simple neopixel color cycle demo on stm32f405 feather.
#include <Adafruit_NeoPixel.h>

#define PIXEL_PIN    8 // Digital IO pin connected to the NeoPixels.
#define PIXEL_COUNT  1 // Number of NeoPixels

// Declare our NeoPixel strip object.
Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);

void setup() {
  initializeLED();
  strip.begin();           // Initialize NeoPixel strip object 
  strip.show();            // Initialize all pixels to 'off'
  strip.setBrightness(50); // Set BRIGHTNESS (max = 255)
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() { 
  const uint8_t rgb[6] = { B100, B101, B001, B011, B010, B110 };
  
  for (int i=0; i<6; i++) {
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_1);
    for (uint8_t j=0x11; j<0xff; j+=0x11) {
      strip.setPixelColor(0, (rgb[i]&B001 ? j : 0), (rgb[i]&B010 ? j : 0), 
                          (rgb[i]&B100 ? j : 0));
      strip.show();
      delay(50);
    }
  }
}

Simple Neopixel Demonstration on the STM32F405 Feather Using FreeRTOS

#include <STM32FreeRTOS.h>
#include <queue.h>
#include <Adafruit_NeoPixel.h>

#define PIXEL_PIN    8 // Digital IO pin connected to the NeoPixels.
#define PIXEL_COUNT  1 // Number of NeoPixels

// Declare our NeoPixel strip object.
Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);

struct rgb {
  uint8_t red;
  uint8_t green;
  uint8_t blue;
};

QueueHandle_t q = 0;

void producer(void* p) {
  while (1) {
    const uint8_t rgbMix[6] = { B100, B101, B001, B011, B010, B110 };
    struct rgb color = { 0, 0, 0 };
  
    for (int i=0; i<6; i++)  {
      for (uint8_t j=0x11; j<0xff; j+=0x11)  {
        color.red =   rgbMix[i] & B001 ? j : 0;
        color.green = rgbMix[i] & B010 ? j : 0;
        color.blue =  rgbMix[i] & B100 ? j : 0;
        if (!xQueueSend(q, &color, 100)) 
            while (1);
        vTaskDelay(50);
      }
    }
  }
}

void consumer(void* p) {
  strip.begin();           // Initialize NeoPixel strip object 
  strip.show();            // Initialize all pixels to 'off'
  strip.setBrightness(50); // Set BRIGHTNESS (max = 255)

  while (1) {
    struct rgb led;
    
    if (!xQueueReceive(q, &led, 100)) 
      while (1);
    else {
      strip.setPixelColor(0, led.red, led.green, led.blue);
      strip.show();
    }
  }
}

void setup() {
  portBASE_TYPE s1, s2;

  q = xQueueCreate(1, sizeof(struct rgb));
  s1 = xTaskCreate(producer, "tx", 128, 0, 1, 0);
  s2 = xTaskCreate(consumer, "rx", 128, 0, 1, 0);
  
  if (q == NULL || s1 != pdPASS || s2 != pdPASS) 
    while(1);
  
  vTaskStartScheduler();
  while (1);
}

void loop() { }
Posted in Uncategorized | Leave a comment

Arduino GPS-based Lap Timer Revisited

GPS-based lap timing code is located here. The source code provided is an MSVC program used for testing purposes. This is simply the basic algorithms needed to function. Very little error checking is performed by the program. It can easily be modified to work on a laptop using a GPS connected through the USB. With minimal modification, it could also be adapted for use on a microcontroller. A text-based file of GPS RMC sentences recorded from several laps of the Portland International Raceway circuit can be used with the program and is located in my github repository.

This is a work in progress [updated on 1/13/2020].

The main loop of the program looks like this:

	// Main gps string processing loop.
	while (1)
	{
		if (!GetRMCSentence(port, tokens))
		{
			std::cout << error.GetDescription() << std::endl;
			continue;
		}

		// Previous position gps time stamp.
		float prevTimeStamp = timeStamp;
		
		// Confirm sentence is sequential.
		timeStamp = atof(tokens[RMC_TIME]);
		if (!Equal(timeStamp, prevTimeStamp + GPS_UPDATE_PERIOD) && !Equal(timeStamp, prevTimeStamp + 40. + GPS_UPDATE_PERIOD))
		{
			error.SetError(err::ID::TIME_STAMP);
			std::cout << error.GetDescription() << std::endl;
			continue;
		}

		// Get current track position (lat, long).
		if (tokens[RMC_LATITUDE] != nullptr || tokens[RMC_LONGITUDE] != nullptr)
		{
			char temp[12];

			GeoCopy(tokens[RMC_LATITUDE], temp, LATITUDE);
			track.p1.x = atof(temp);
			GeoCopy(tokens[RMC_LONGITUDE], temp, LONGITUDE);
			track.p1.y = atof(temp);
		}
		else
			continue;

		// Ignore gps sentences for 1 second after crossing start/finish.
		if (hzCounter < GPS_UPDATE_FREQUENCY)
		{
			hzCounter++;
			// Prepare for next iteration.
			track.p0.x = track.p1.x;
			track.p0.y = track.p1.y;
			continue;
		}
		
		// Heading sanity check & check if crossed start/finish line?
		if (Within30(startHeading, (uint16_t)atol(tokens[RMC_TRACK])) && LineIntersection(track))
		{
			point_t intersectPoint;

			// Calculate track/start line intersection point.
			IntersectPoint(track.p0, track.p1, &intersectPoint);

			// Overall length of this track segment.
			float totDist = Distance(track.p0, track.p1);

			// Length from start line intersection point to track segment end point.
			float segDist = Distance(intersectPoint, track.p1);

			// Calculate startline crossing time for this and next lap.
			float xTime = timeStamp - (GPS_UPDATE_PERIOD * (segDist / totDist));
			lapData[numLaps].setStop(xTime);
			lapData[numLaps + 1].setStart(xTime);

			// Determine current lap stats.
			DisplayTime(numLaps + 1, lapData[numLaps].getTime());
			if (numLaps > 0)
				DisplayTime(bestTime.first + 1, bestTime.second);

			// Is this lap a new best?
			if (numLaps == 0 || lapData[numLaps].getTime() < bestTime.second)
			{
				// Announce new fast lap.
				std::cout << " << Fast Lap";
				bestTime = std::make_pair(numLaps, lapData[numLaps].getTime());
			}
			std::cout << "\n";

			// Increment counters.
			numLaps++;
			hzCounter = 1;
		}

		// Prepare for next iteration.
		track.p0.x = track.p1.x;
		track.p0.y = track.p1.y;
	}
Posted in arduino, c | Tagged , , | Leave a comment

Arduino BBQ Fan Temperature Controller

Commercially sourced BBQ temperature controllers are expensive. They start at about US $200 and average well above $300. Most include WIFI interfaces too. So I thought I could do better. This is the first step in making a BBQ temperature controller. The next step will be to add a simple flue control. Eventually, I want to add WIFI control to the device.

This project uses the following components. Most of my parts were sourced off ebay and came from China.

  • Arduino
  • 12v fan
  • 12v power supply
  • L298N motor control module
  • MAX6675 k-type thermocouple module
  • TM-1637 4-digit 7-segment display
  • 2 switches

Obviously, the fan is one of the most important items in how well the system operates. Fans are rated by their airflow, typically given in Cubic Feet per Minute (CFM). Here is an example of a nominally-sized, fan with a decent rating of 40 CFM at 12 volts.

So far, the system simply allows the user to select a “set-point” temperature and stores this value in EEPROM. An attached MAX6675 k-type thermocouple (0-500 degree C range) is used to check the temperature inside the BBQ. The variable fan motor speed is adjusted depending upon the temperature difference between the actual and set-point values. Signals sent from the Arduino to the L298N module are the commands to set a specific fan speed. An additional benefit to using an L298N module is it has a 5V output capable of supplying the other components. The two buttons are used to display and adjust the “set-point” temperature. A TM-1637 4-digit, 7-segment display is incorporated to show either temperatures.

Finer temperature control will be achieved through use of a flue door. This will help to prevent temperature overshoots and allow for the full range of temperature throttling. I envision using a flue door operated via a servo and possibly incorporating software PID control if necessary.

The complete system can be viewed below:

Here is a matrix showing all of the interconnections between modules. It’s really not as difficult as it looks:

The project consumes 10 digital pins on the Arduino. And while I used an old Atmega168 based Arduino, a 328, Uno or Leonardo would work equally as well. The Arduino firmware is very simple:

/*
 * Temperature Controlled BBQ Variable Fan
 * James M. Eli
 * 8/20/2019
 * Version 1.0
 * 
 * Components:
 *   Arduino (Atmega168)
 *   MAX6675 K-Type Thermocouple
 *   TM1637 7-segment 4-digit Display
 *   L298N Motor Contorller Module
 *   12V Fan
 *   12V Power Supply
 *   
 * Sketch uses 4096 bytes (28%) of program storage space. 
 * Maximum is 14336 bytes.
 * Global variables use 47 bytes (4%) of dynamic memory, 
 * leaving 977 bytes for local variables. 
 * Maximum is 1024 bytes.
 */
#include "TM1637Display.h" // display module
#include "max6675.h"       // temperature module
#include "EEPROM.h"        // eeprom access

// Program states.
constexpr uint8_t NO_STATE{ 0 };
constexpr uint8_t INC_TEMP{ 1 };
constexpr uint8_t DEC_TEMP{ 2 };
constexpr uint8_t SET_TEMP{ 3 };

// TM1637 display module connection pins.
constexpr uint8_t TM1637_CLK { 2 };   // PD2
constexpr uint8_t TM1637_DIO { 3 };   // PD3
// MAX6675 temperature module connection pins.
constexpr uint8_t MAX6675_SO { 8 };   // PB0
constexpr uint8_t MAX6675_CS { 9 };   // PB1
constexpr uint8_t MAX6675_CLK { 10 }; // PB2
// L298N module connection pins.
constexpr uint8_t L298N_ENA { 5 };    // PD5, l298n pin #7 (motor 1 enable, remove jumper)
constexpr uint8_t L298N_IN1 { 7 };    // PD7, l298n pin #8 (IN1)
constexpr uint8_t L298N_IN2 { 6 };    // PD6, l298n pin #9 (IN2)
// Buttons (2).
constexpr uint8_t INC_BUTTON { 11 };  // PB3
constexpr uint8_t DEC_BUTTON { 12 };  // PB4

// Fan speed temperature thresholds (degrees F).
constexpr int16_t TEMP_THRESHOLD[4] = { 7, 16, 33, 65 };
// Fan speeds voltages (0-255, where 255=100%).
constexpr uint8_t FAN_SPEED[4] = { 63, 127, 191, 255 };

// Temperature value change per each increase/decrease.
constexpr int16_t TEMP_STEP { 10 };
// Min, max and default temperature settings.
constexpr int16_t MIN_TEMP { 200 };
constexpr int16_t MAX_TEMP { 500 };
constexpr int16_t DEFAULT_TEMP { 350 };

// Eeprom address (stores set point temperature).
#define EEPROM_ADDRESS 0 

// Display timer.
uint32_t timer;
// Set point temperature & actual temperature (as read from MAX6675).
int16_t setTemp;
int16_t actTemp;
// Pointer to select temperature for display.
int16_t *temp = &actTemp;

// Instantiate display & temperature module objects.
TM1637Display disp(TM1637_CLK, TM1637_DIO);
MAX6675 tc(MAX6675_CLK, MAX6675_CS, MAX6675_SO);

// Read/write word to/from EEPROM.
int16_t eepromRead16(uint16_t address) 
{
  int16_t value = word(EEPROM.read(address), EEPROM.read(address + 1));
  return value;
} 
void eepromWrite16(uint16_t address, int16_t value) 
{
  EEPROM.write(address, highByte(value));
  EEPROM.write(address + 1, lowByte(value));
}

// Retrieve temperature from eeprom.
int16_t getEepromTemp(void) 
{
  int16_t temp;

  // Get temp from eeprom memory.
  temp = eepromRead16((uint16_t)EEPROM_ADDRESS);

  // Validate temp in range & multiple of step value.
  temp = (temp / TEMP_STEP) * TEMP_STEP;
  if (temp < MIN_TEMP || temp > MAX_TEMP)
    temp = DEFAULT_TEMP;
  
  // Save it.
  eepromWrite16((uint16_t)EEPROM_ADDRESS, (int16_t)temp);
  return temp;
}

// Store temperature in eeprom.
void setEepromTemp(int16_t temp) 
{
  // Validate temp in range & multiple of step value.
  temp = (temp / TEMP_STEP) * TEMP_STEP;
  if (temp < MIN_TEMP || temp > MAX_TEMP)
    temp = DEFAULT_TEMP; 
  
  // Save it.
  eepromWrite16((uint16_t)EEPROM_ADDRESS, (int16_t)temp);  
}

// Poll buttons.
uint8_t checkButtons() 
{ 
  // Both buttons down?
  if ( !(PINB&(1<<PORTB3)) && !(PINB&(1<<PORTB4)) ) 
    return SET_TEMP;
  
  // Increment button down?
  else if ( !(PINB&(1<<PORTB3)) ) 
    return INC_TEMP; 
  
  // Decrement button down?
  else if ( !(PINB&(1<<PORTB4)) ) 
    return DEC_TEMP; 
  
  return NO_STATE;
}

void setup() 
{
  // L298N motor controller pins.
  pinMode(L298N_ENA, OUTPUT);
  pinMode(L298N_IN1, OUTPUT);
  pinMode(L298N_IN2, OUTPUT);
  
  // TM1637 display module connection pins.
  pinMode(TM1637_CLK, OUTPUT);
  pinMode(TM1637_DIO, OUTPUT);
  
  // MAX6675 temperature module connection pins.
  pinMode(MAX6675_SO, OUTPUT);
  pinMode(MAX6675_CS, OUTPUT);
  pinMode(MAX6675_CLK, OUTPUT);
  
  // Activate internal pull-ups on button pins.
  pinMode(INC_BUTTON, INPUT);
  pinMode(INC_BUTTON, INPUT_PULLUP);
  pinMode(DEC_BUTTON, INPUT);
  pinMode(DEC_BUTTON, INPUT_PULLUP);

  setFan(0);                 // Turn fan motor off.
  disp.setBrightness(0);     // Dim down.
  timer = millis();          // Initialize timer.
  setTemp = getEepromTemp(); // Retrieve stored set point temperature.
  delay(500);                // Allow everything to settle.
}

// Update L298N fan motor.
void setFan(uint8_t index) 
{
  if (index == 0) 
  {
    // Turn fan motor off.
    digitalWrite(L298N_IN1, LOW);
    digitalWrite(L298N_IN2, LOW);  
  } 
  else 
  {
    // Turn motor on and adjust speed.
    digitalWrite(L298N_IN1, HIGH);
    digitalWrite(L298N_IN2, LOW);
    analogWrite(L298N_ENA, FAN_SPEED[index]);
  }
}

// Handle TM1637 display details.
void displayTemp(int16_t t) 
{
  disp.setBrightness(0x0f);
  //disp.clear();
  disp.showNumberDec(t, false);  // Show decimal numbers without leading zeros
}

void loop() 
{
  actTemp = (int16_t)tc.readFahrenheit();
  uint8_t button = checkButtons();

  if (button) 
  {
    // Reset display timer.
    timer = millis(); 
    if (temp != &setTemp)
      // Display set point temperature.
      temp = &setTemp;
    else if (button == INC_TEMP)
    {
      // Increment temperature by step amount.
      setTemp += TEMP_STEP;
      if (setTemp > MAX_TEMP)
        setTemp = MIN_TEMP;
      setEepromTemp(setTemp);
    }
    else if (button == DEC_TEMP)
    {
      // decrement temperature by step amount.
      setTemp -= TEMP_STEP;
      if (setTemp < MIN_TEMP)
        setTemp = MAX_TEMP;
      setEepromTemp(setTemp);
    }
  }
  else
  {
    uint8_t speed { 0 };
    // Get temperature difference.
    int16_t dTemp = (int16_t)(setTemp - actTemp);

    // Iterate through temperature thresholds, incrementing fan speed.
    for (int i=0; i<4; i++)
      if (dTemp >= TEMP_THRESHOLD[i])
        speed++;

    setFan(speed);
  }
    
  // Display actual or set point temperature.
  displayTemp((int16_t)*temp);
  
  // A delay is useful for switch debounce.
  if (temp == &setTemp) {
    // Time to change display temperature to actual?
    if (millis() - timer > 5000)
      temp = &actTemp;
    else
    delay(500);
  }
  else
    delay(1000);
}
Posted in arduino | Tagged | 9 Comments

Blynk-ing an ANAVI Light Controller

The ANAVI Light Controller is an open source hardware WiFi development board. As the name suggests it controls 12V RGB LED strip lights and can retrieve data from various I2C sensor modules for temperature, humidity, light and gestures. In this application, I am not using any of these sensors, however, in the future I do intend to experiment with an APDS-9960, primarily for gesture detection.

I want to incorporate LED strip lighting into my existing home automation project. This project is primarily controlled via a Blynk application on my android phone. In this demonstration project, the Blynk application requires 4 slider widgets- one each for the R, G, B colors and an additional one for controlling overall brightness of the LEDs. An additional button widget can be added to provide instant on/off capability and a timer widget could be used for scheduled operation. I further use the functionality of the button to incorporate Alexa voice commands. Use of the timer widget option is not discussed here.

At the heart of the ANAVI Light Controller is an ESP8266-12E. The ESP8266 programming pins are exposed, so the ANAVI board can easily be re-flashed. GPIO0 of ESP8266 is connected to the button called SW1 allowing effortless entry into the programming mode. Furthermore, there is a small red indication LED marked as D1 which could be used for various purposes. In my application, I use this red LED to signal completion of device setup. In order to flash new firmware on ANAVI Light Controller you need a USB to UART serial debug cable and a 12V power supply. Full instruction for re-flashing can be found here.

My replacement firmware code is below. For this project I utilized the PlatformIO IDE with Visual Studio Code, however the Arduino IDE development environment could also be used. In my application, the Alexa voice commands simply allow turning all of the LEDS on and off and is not used for color selection. Also, please note my choice of Blynk virtual pin numbers is purely arbitrary. Good luck.

/*************************************************************************
 * Title: Simple ESP-8266 Wifi RGB LED Strip Light Controller
 * File: main.cpp (esp8266_anavi_rgb_led_controller.ino)
 * Author: James Eli
 * Date: 4/19/2019
 *
 * Program controls an RGB LED strip light via ANAVI light controller device. 
 *
 *************************************************************************
 * 4/19/2019: Migrated to platformio. JME
 *************************************************************************/
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <WiFiManager.h> 
#include <ESP8266WebServer.h>
#include <BlynkSimpleEsp8266.h>
#include <WiFiUdp.h>
#include <SimpleTimer.h>
#include "WemoSwitch.h"
#include "WemoManager.h"
#include "CallbackFunction.h"
#include <ArduinoOTA.h>

// Blynk App authentication token, wifi ssid and password.
char auth[] = "...";
char ssid[] = "...";
char pass[] = "...";

// Amazon echo response term & Wifi access point name.
#define ECHO_KEY_WORD "cabinets"

// Maximum brightness.
#define MAX_BRIGHTNESS 255

// Esp8266 pins.
const int ESP8266_RED_PIN   = 12;
const int ESP8266_GREEN_PIN = 13;
const int ESP8266_BLUE_PIN  = 14;
const int ALARM_PIN = 16;
// Blynk virtual pin vs. anavi hw pin assignments.
// Red   = Blynk virtual pin V30 = anavi pin 12
// Green = Blynk virtual pin V31 = anavi pin 13
// Blue  = Blynk virtual pin V32 = anavi pin 14
// V29 = BUTTON
// RGB slider values (0-255).
int red, green, blue;
// Brightness slider (0-255).
int brightness;
// Button status.
int button;
// Connect resync flag.
bool isFirstConnect = true;

WemoManager wemoManager;
WemoSwitch *device = NULL;

void otaStarted() { Serial.println("OTA update started!"); }
void otaFinished() { Serial.println("OTA update finished!"); }
void otaProgress(unsigned int progress, unsigned int total) { Serial.printf("OTA progress: %u%%\r", (progress / (total / 100))); }
void otaError(ota_error_t error) {
    Serial.printf("Error [%u]: ", error);
    switch (error) {
        case OTA_AUTH_ERROR:
            Serial.println("auth failed!");
            break;
        case OTA_BEGIN_ERROR:
            Serial.println("begin failed!");
            break;
        case OTA_CONNECT_ERROR:
            Serial.println("connect failed!");
            break;
        case OTA_RECEIVE_ERROR:
            Serial.println("receive failed!");
            break;
        case OTA_END_ERROR:
            Serial.println("end failed!");
            break;
    }
}

// Turn all LED lines off.
void LEDsOff() 
{
  analogWrite(ESP8266_RED_PIN, 0);
  analogWrite(ESP8266_GREEN_PIN, 0);
  analogWrite(ESP8266_BLUE_PIN, 0);
}

// Turn LEDS on.
void LEDsOn() 
{
  analogWrite(ESP8266_RED_PIN, (red*brightness)/MAX_BRIGHTNESS);
  analogWrite(ESP8266_GREEN_PIN, (green*brightness)/MAX_BRIGHTNESS);
  analogWrite(ESP8266_BLUE_PIN, (blue*brightness)/MAX_BRIGHTNESS);
}

void setup() 
{
  // LED.
  pinMode(ALARM_PIN, OUTPUT);
  digitalWrite(ALARM_PIN, HIGH);

  // WiFiManager intialization.
  WiFiManager wifi;               

  // Init pins.
  pinMode( ESP8266_RED_PIN, OUTPUT );
  pinMode( ESP8266_GREEN_PIN, OUTPUT );
  pinMode( ESP8266_BLUE_PIN, OUTPUT );

  // Set ota details.
  ArduinoOTA.onStart(otaStarted);
  ArduinoOTA.onEnd(otaFinished);
  ArduinoOTA.onProgress(otaProgress);
  ArduinoOTA.onError(otaError);
  ArduinoOTA.setHostname("cabinets");
  ArduinoOTA.begin();

  // Create AP, if necessary
  wifi.autoConnect( ECHO_KEY_WORD ); 
  // wemoManager used for alexa interface.
  wemoManager.begin();

  // Set WIFI module to STA mode
  WiFi.mode( WIFI_STA );

  // Format: Alexa invocation name, local port no, on callback, off callback
  device = new WemoSwitch( ECHO_KEY_WORD, 80, LEDsOn, LEDsOff );
  wemoManager.addDevice( *device );

  // Report success.
  digitalWrite(ALARM_PIN, LOW);

  // Initialize Blynk.
  Blynk.config( auth );
}

void loop() 
{
  wemoManager.serverLoop();
  ArduinoOTA.handle();
  Blynk.run();
}

void syncPins()
{
  LEDsOn();

  if (red || green || blue || brightness)
  { 
    Blynk.virtualWrite(V29, 1);
    button = 1;
  }
  else 
  {
    Blynk.virtualWrite(V29, 0);
    button = 0;
  }
}

// Read value from r, g, b and brightness sliders.
BLYNK_WRITE(V30) { red = param.asInt();  syncPins(); }
BLYNK_WRITE(V31) { green = param.asInt(); syncPins(); }
BLYNK_WRITE(V32) { blue = param.asInt(); syncPins(); }
BLYNK_WRITE(V33) { brightness = param.asInt(); syncPins(); }

// Button.
BLYNK_WRITE(V29)
{
    button = param.asInt();
    
    if (button == 0) 
      LEDsOff();
    else if (red || green || blue || brightness)
      LEDsOn();
    else 
    {
      Blynk.virtualWrite(V29, 0);
      button = 0;
    }  
}

BLYNK_CONNECTED()
{
  if (isFirstConnect)
  {
    Blynk.syncAll();
    isFirstConnect = false;
  }
}
Posted in arduino, iot | Tagged , , , | Leave a comment

Controlling an Itead Sonoff B1 Color LED Bulb Using Blynk

The Itead Sonoff B1 color LED bulb is a 6W (2A maximum), 600lm output, RGB full color, dimmable bulb with an integrated ESP8285 WIFI chip. As purchased it can be controlled via the proprietary EWeLink application. However, I wanted to incorporate the bulb into my existing home automation project which is primarily controlled via a Blynk application on my android phone.

In order to fully control the bulb, the Blynk application will require at least 5 slider widgets (one each for the RGB, cool and white colors). An additional button widget can be added to provide instant on/off capability and a timer widget could be used for scheduled operation. Use of the timer widget option is not discussed here.

A quick search of the internet revealed directions on how to disassemble, connect to, and re-flash the ESP8285 chip. This process required soldering 4 wires to test points on the bulb’s PCB. The wires are then used to connect a USB-to-Serial type programmer. After reprogramming is completed, the wires are detached. For simplicity, I utilized the Arduino IDE development environment with the ESP8266 add-on for all of the programming needs.

To disassemble the bulb, I used a small plastic auto trim tool to assist in the removal of the translucent cover. My cover was lightly tacked in place through the use of 2 very small dabs of an extremely weak glue. I simply applied light pressure working my small plastic wedge around the perimeter of the cover, which easily separated. The cover simply snapped back in place.

My replacement firmware code is below. Please note my choice of Blynk virtual pin numbers is purely arbitrary. Unlike other advice found on the Internet, I used the Arduino IDE’s Generic ESP8285 board, 1M (64 SPIFFS) flash size and DOUT upload settings during programming. Good luck.

/*************************************************************************
 * Title: Simple ESP-8266 Wifi Sonoff B1 LED Light Controller
 * File: sonoff_echo_blynk_sonoff_b1.ino
 * Author: James Eli
 * Date: 2/7/2019
 *
 * This program controls a Sonoff B1 RGB LED light via the blynk app. 
 * 
 * Notes:
 *  (1) To place an ESP8266 into program mode, GPIO0 must be LOW during power up. 
 *  (2) See: https://github.com/arendst/Sonoff-Tasmota/wiki/Sonoff-B1-and-B1-R2
 *  and: https://tinkerman.cat/sonoff-b1-lights-and-shades/
 *        
 * Upload Settings:
 *   Board: Generic Generic ESP8285
 *   Flash size: 1M (64 SPIFFS)
 *   Flash Mode: DOUT
*************************************************************************
 * Change Log:
 *   1/14/2018: Initial release. JME
 *************************************************************************/
#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>

// Blynk App authentication token, wifi ssid and password.
char auth[] = "..."; 
char ssid[] = "...";
char pass[] = "...";

// Sonoff B1/esp8266 pins.
#define DI_PIN  12 // GPIO12 
#define DCK_PIN 14 // GPIO14 
// Blynk virtual pins.
// V15 = red slider [0-255].
// V16 = green slider [0-255].
// V17 = blue slider [0-255].
// V19 = button [0-1]
// V20 = warm white slider [0-255].
// V21 = cool white slider [0-255].

// Slider values, initally set OFF.
int red = 0;
int green = 0;
int blue = 0;
int warmWhite = 0;
int coolWhite = 0;
// Button status.
int button = 0;

// Connect resync flag.
bool isFirstConnect = true;

// See MY9231 driver library @ https://github.com/xoseperez/my92xx
void pulseDI(uint8_t times)
{
  for (uint8_t i = 0; i < times; i++) 
  {
    digitalWrite(DI_PIN, HIGH);
    digitalWrite(DI_PIN, LOW);
  }
}

void pulseDCK(uint8_t times)
{
  for (uint8_t i = 0; i < times; i++) 
  {
    digitalWrite(DCK_PIN, HIGH);
    digitalWrite(DCK_PIN, LOW);
  }
}

void writeData(uint8_t data)
{
  // Send 8-bit data.
  for (uint8_t i = 0; i < 4; i++) 
  {
    digitalWrite(DCK_PIN, LOW);
    digitalWrite(DI_PIN, (data & 0x80));
    digitalWrite(DCK_PIN, HIGH);
    data = data << 1;
    digitalWrite(DI_PIN, (data & 0x80));
    digitalWrite(DCK_PIN, LOW);
    digitalWrite(DI_PIN, LOW);
    data = data << 1;
  }
}

void setupLED()
{
  // GPIO setup.
  pinMode(DI_PIN, OUTPUT);
  pinMode(DCK_PIN, OUTPUT);
  
  pulseDCK(64);                           // Clear all duty registers (2 chipes * 32).
  delayMicroseconds(12);                  // TStop > 12us.
  // Send 12 DI pulse, after 6 pulse's falling edge store duty data, and 12
  // pulse's rising edge convert to command mode.
  pulseDI(12);
  delayMicroseconds(12);                  // Delay >12us, begin send CMD data.
  // Send CMD data
  for (uint8_t n = 0; n < 2; n++)         // 2 chips in SONOFF B1.
    writeData(0x18);                      // ONE_SHOT_DISABLE, REACTION_FAST, BIT_WIDTH_8, FREQUENCY_DIVIDE_1, SCATTER_APDM
  delayMicroseconds(12);                  // TStart > 12us. Delay 12 us.
  // Send 16 DI pulse, at 14 pulse's falling edge store CMD data, and
  // at 16 pulse's falling edge convert to duty mode.
  pulseDI(16);
  delayMicroseconds(12);                  // TStop > 12us.
}

void setLED(uint8_t r, uint8_t g, uint8_t b, uint8_t w, uint8_t c)
{
  uint8_t duty[6] = { w, c, 0, g, r, b }; // RGBWC channels.

  delayMicroseconds(12);                  // TStop > 12us.
  for (uint8_t channel = 0; channel < 6; channel++) 
    writeData(duty[channel]);             // Send 8-bit Data.
  delayMicroseconds(12);                  // TStart > 12us. Ready for send DI pulse.
  pulseDI(8);                             // Send 8 DI pulse. After 8 pulse falling edge, store old data.
  delayMicroseconds(12);                  // TStop > 12us.
}

void setup() 
{
  // Init bulb LEDs.
  setupLED();
  // Set LEDs (initially off).
  setLED(red, green, blue, warmWhite, coolWhite);
  // Start Blynk.
  Blynk.begin(auth, ssid, pass);
}

void loop() { Blynk.run(); }

// Sync Blynk app and bulb. 
void syncPins()
{
  setLED(red, green, blue, warm, cool);

  if (button == 0 && (red || blue || green || warmWhite || coolWhite))
  { 
    Blynk.virtualWrite(V19, 1);
    button = 1;
  }
  
  if (button == 1 && !red && !blue && !green && !warmWhite && !coolWhite)
  {
    Blynk.virtualWrite(V19, 0);
    button = 0;
  }
}

// Read value from red, green, blue and brightness sliders.
BLYNK_WRITE(V15) { red = param.asInt(); syncPins(); }
BLYNK_WRITE(V16) { green = param.asInt(); syncPins(); }
BLYNK_WRITE(V17) { blue = param.asInt(); syncPins(); }
BLYNK_WRITE(V20) { warm = param.asInt(); syncPins(); }
BLYNK_WRITE(V21) { cool = param.asInt(); syncPins(); }

// Process Blynk ON/OFF button.
BLYNK_WRITE(V19)
{
  button = param.asInt();

  if (button == 1 && (red || blue || green || warmWhite || coolWhite))
    setLED(red, green, blue, warm, cool);
  else
  {
    setLED(0, 0, 0, 0, 0);
    Blynk.virtualWrite(V19, 0);
    button = 0;
  }
}

// Initial wifi connection sync.
BLYNK_CONNECTED()
{
  if (isFirstConnect)
  {
    Blynk.syncAll();
    isFirstConnect = false;
  }
}
Posted in iot | Tagged , , | Leave a comment

Using a Sonoff S31 with Blynk

testing

This post will describe how the Itead Sonofff S31 can control a household AC electric device with a cellphone loaded with the Blynk application. Additionally, we can use the S31 to monitor energy usage by keeping track of real-time power, current and voltage of the connected appliance.

s31

A Blynk timer widget allows automatic on/off control via a time schedule. The provided source code also includes Amazon Alexa voice command compatibility.

screenshot_2019-01-21-08-08-56

The Sonoff S31 incorporates an CSE7766 single-phase, multi-function measuring chip, which provides measurements of electric current, voltage and power through the ESP8266 UART. The CSE7766 operates at a baud rate of 4800bps (± 2%), with 8-bit data, 1 even parity check and 1 stop bit. The datasheet contains all the details and can be downloaded here.

powermeter

The S31 device was constructed in such a way that taking them apart is easy. You will need to solder 4 wires or a 4-pin header to the test points on the exposed PCB in order to upload the new firmware. Instead of duplicating the instructions for disassembly and reflashing of the Sonoff S31, simply see the blog here.

/*************************************************************************
 * Title: Simple ESP-8266 Amazon Echo/sonoff wifi relay control
 * File: sonoff_echo_blynk_Utility.ino
 * Author: James Eli
 * Date: 1/20/2019
 * 
 * Sonoff S31 ESP8266 "utility".
 *   Uses Blynk App V0 (switch), V1 (indicator LED), V2 (timer), V3 (voltage), 
 *   V4 (current), V5 (power), V6 (energy)
 *
 * This program controls a Sonoff wifi relay module communicating either 
 * through an amazon echo or the Blynk android application to the Sonoff 
 * onboard esp-8266 module. Amazon Echo responds to ECHO_KEY_WORD.
 *************************************************************************/
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <WiFiManager.h> 
#include <ESP8266WebServer.h>
#include <BlynkSimpleEsp8266.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <SimpleTimer.h>
#include "WemoSwitch.h"
#include "WemoManager.h"
#include "CallbackFunction.h"

bool isFirstConnect = true;       // Flag for re-sync on connection.
int relayState = LOW;             // Blynk app pushbutton status.

// Blynk app authentication code.
char auth[] = "...";

boolean SwitchReset = true;       // Flag indicating that the hardware button has been released

// esp8266 pins.
#define ESP8266_GPIO13  13        // Sonof green LED (LOW == ON).
#define ESP8266_GPIO0   0         // Sonoff pushbutton (LOW == pressed).
#define ESP8266_GPIO12  12        // Sonoff relay (HIGH == ON).
const int RELAY = ESP8266_GPIO12; // Relay switching pin. Relay is pin 12 on the SonOff
const int LED = ESP8266_GPIO13;   // On/off indicator LED. Onboard LED is 13 on Sonoff
const int SWITCH = ESP8266_GPIO0; // Pushbutton.

// CSE7766 data.
double power = 0;
double voltage = 0;
double current = 0;
double energy = 0;
double ratioV = 1.0;
double ratioC = 1.0;
double ratioP = 1.0;
// Serial data input buffer.
unsigned char serialBuffer[24];
// Serial error flags.
int error;
// Energy reset counter.
int energyResetCounter;
#define MAX_ENREGY_RESET_COUNT 12

// CSE7766 error codes.
#define SENSOR_ERROR_OK             0       // No error.
#define SENSOR_ERROR_OUT_OF_RANGE   1       // Result out of sensor range.
#define SENSOR_ERROR_WARM_UP        2       // Sensor is warming-up.
#define SENSOR_ERROR_TIMEOUT        3       // Response from sensor timed out.
#define SENSOR_ERROR_UNKNOWN_ID     4       // Sensor did not report a known ID.
#define SENSOR_ERROR_CRC            5       // Sensor data corrupted.
#define SENSOR_ERROR_I2C            6       // Wrong or locked I2C address.
#define SENSOR_ERROR_GPIO_USED      7       // The GPIO is already in use.
#define SENSOR_ERROR_CALIBRATION    8       // Calibration error or not calibrated.
#define SENSOR_ERROR_OTHER          99      // Any other error.
#define CSE7766_V1R                 1.0     // 1mR current resistor.
#define CSE7766_V2R                 1.0     // 1M voltage resistor.

// Amazon echo response term & Wifi access point name.
#define ECHO_KEY_WORD "utility"

WemoManager wemoManager;
WemoSwitch *device = NULL;

SimpleTimer timer;

void setup() 
{
  // WiFiManager intialization.
  WiFiManager wifi;               

  // Initialize pins.
  pinMode( RELAY, OUTPUT );
  pinMode( LED, OUTPUT );
  pinMode( SWITCH, INPUT_PULLUP );
  delay( 10 );
  // Switch relay off, LED on.
  digitalWrite( RELAY, LOW );
  digitalWrite( LED, LOW ); 

  // Create AP, if necessary
  wifi.autoConnect( ECHO_KEY_WORD ); 
  // wemoManager used for alexa interface.
  wemoManager.begin();

  // Set WIFI module to STA mode
  WiFi.mode( WIFI_STA );
  
  // Format: Alexa invocation name, local port no, on callback, off callback
  device = new WemoSwitch( ECHO_KEY_WORD, 80, RelayOn, RelayOff );
  wemoManager.addDevice( *device );

  // Initialize Blynk.
  Blynk.config( auth );

  // Set ota details.
  ArduinoOTA.setHostname( "utility" );
  ArduinoOTA.begin();

  // Setup cse7766 serial.
  Serial.flush();
  Serial.begin( 4800 );

  // Start a timer for checking button presses @ 100ms intervals.
  timer.setInterval( 100, ButtonCheck );

  // Start a timer for checking cse7766 power monitor @ 1000ms intervals.
  timer.setInterval( 1000, ReadCse7766 );
  
  // Switch LED off to signal initialization complete.
  digitalWrite( LED, HIGH );
}

// Main program loop.
void loop() 
{
  wemoManager.serverLoop();
  ArduinoOTA.handle();
  Blynk.run();
  timer.run();
}

// Toggle the relay on
void RelayOn() 
{
  digitalWrite( RELAY, HIGH );
  relayState = true;
  Blynk.virtualWrite( V0, HIGH ); // Sync the Blynk button widget state
  Blynk.virtualWrite( V1, relayState*255 );
}

// Toggle the relay off
void RelayOff() 
{
  digitalWrite( RELAY, LOW );
  relayState = false;
  Blynk.virtualWrite( V0, LOW ); // Sync the Blynk button widget state
  Blynk.virtualWrite( V1, relayState*255 );
}

// Handle switch changes originating on the Blynk app
BLYNK_WRITE( V0 ) 
{
  int SwitchStatus = param.asInt();
  
  if ( SwitchStatus )
    RelayOn();
  else 
    RelayOff();
}

// Handle timer changes originating from the Blynk app.
BLYNK_WRITE( V2 ) 
{
  int SwitchStatus = param.asInt();
  
  if ( SwitchStatus )
    RelayOn();
  else 
    RelayOff();
}

// This function runs every time Blynk connection is established.
BLYNK_CONNECTED() 
{
  if ( isFirstConnect ) 
  {
    Blynk.syncAll();
    isFirstConnect = false;
  }
}

// Handle hardware switch activation.
void ButtonCheck() 
{
  // look for new button press
  boolean SwitchState = ( digitalRead( SWITCH ) );
  
  // toggle the switch if there's a new button press
  if ( !SwitchState && SwitchReset == true ) 
  {
    if ( relayState )
      RelayOff();
    else
      RelayOn();
  
    // Flag that indicates the physical button hasn't been released
    SwitchReset = false;
    delay( 50 );            // De-bounce interlude.
  } 
  else if ( SwitchState ) 
  {
    // reset flag the physical button release
    SwitchReset = true;
  }
}

// Relay toggle helper function.
void ToggleRelay() 
{
  relayState = !relayState;
  
  if ( relayState ) 
    RelayOn();
  else 
    RelayOff();
}

// CSE7766 checksum.
bool CheckSum() 
{
  unsigned char checksum = 0;
  
  for (unsigned char i = 2; i < 23; i++) 
    checksum += serialBuffer[i];

  return checksum == serialBuffer[23];
}

// Process a cse7766 data packet.
void ProcessCse7766Packet() 
{
  // Confirm packet checksum.
  if ( !CheckSum() ) 
  {
    error = SENSOR_ERROR_CRC;
    return;
  }

  // Check for calibration error.
  if ( serialBuffer[0] == 0xAA ) 
  {
    error = SENSOR_ERROR_CALIBRATION;
    return;
  }
  if ( (serialBuffer[0] & 0xFC) == 0xFC ) 
  {
    error = SENSOR_ERROR_OTHER;
    return;
  }

  // Retrieve calibration coefficients.
  unsigned long coefV = (serialBuffer[2] << 16 | serialBuffer[3] << 8 | serialBuffer[4] );
  unsigned long coefC = (serialBuffer[8] << 16 | serialBuffer[9] << 8 | serialBuffer[10]);
  unsigned long coefP = (serialBuffer[14] << 16 | serialBuffer[15] << 8 | serialBuffer[16]);
  uint8_t adj = serialBuffer[20];

  // Calculate voltage.
  voltage = 0;
  if ( (adj & 0x40) == 0x40 ) 
  {
    unsigned long voltageCycle = serialBuffer[5] << 16 | serialBuffer[6] << 8 | serialBuffer[7];
    voltage = ratioV*coefV/voltageCycle/CSE7766_V2R;
  }

  // Calculate power.
  power = 0;
  if ( (adj & 0x10) == 0x10 ) 
  {
    if ( (serialBuffer[0] & 0xF2) != 0xF2 ) 
    {
      unsigned long powerCycle = serialBuffer[17] << 16 | serialBuffer[18] << 8 | serialBuffer[19];
      power = ratioP*coefP/powerCycle/CSE7766_V1R/CSE7766_V2R;
    }
  }

  // Calculate current.
  current = 0;
  if ( (adj & 0x20) == 0x20 ) 
  {
    if ( power > 0 ) 
    {
      unsigned long currentCycle = serialBuffer[11] << 16 | serialBuffer[12] << 8 | serialBuffer[13];
      current = ratioC*coefC/currentCycle/CSE7766_V1R;
    }
  }

  // Calculate energy.
  unsigned int difference;
  static unsigned int cfPulsesLast = 0;
  unsigned int cfPulses = serialBuffer[21] << 8 | serialBuffer[22];
  
  if (0 == cfPulsesLast) 
    cfPulsesLast = cfPulses;
  
  if (cfPulses < cfPulsesLast) 
    difference = cfPulses + (0xFFFF - cfPulsesLast) + 1;
  else
    difference = cfPulses - cfPulsesLast;
  
  energy += difference*(float)coefP/1000000.0;
  cfPulsesLast = cfPulses;

  // Energy reset.
  if ( power == 0 )
    energyResetCounter++;
  else
    energyResetCounter = 0;
  if ( energyResetCounter >= MAX_ENREGY_RESET_COUNT )
  {
    energy = 0.0;
    energyResetCounter = 0;
  }
   
  // Push data to Blynk app.
  Blynk.virtualWrite( V3, voltage ); // Voltage (Volts).
  Blynk.virtualWrite( V4, current ); // Current (Amps).
  Blynk.virtualWrite( V5, power );   // Power (Watts).
  Blynk.virtualWrite( V6, energy );  // Energy (kWh).
}

// Read serial cse7766 power monitor data packet.
void ReadCse7766() 
{
  // Assume a non-specific error.
  error = SENSOR_ERROR_OTHER; 
  static unsigned char index = 0;

  while ( Serial.available() > 0 )
  {
    uint8_t input = Serial.read();

    // first byte must be 0x55 or 0xF?.
    if ( index == 0 ) 
    { 
      if ( (input != 0x55) && (input < 0xF0) ) 
        continue;
    }
    // second byte must be 0x5A.
    else if ( index == 1 ) 
    {
      if ( input != 0x5A ) 
      {
        index = 0;
        continue;
      }
    }
    
    serialBuffer[index++] = input;
    
    if ( index > 23 ) 
    {
      Serial.flush();
      break;
    }
  }

  // Process packet.
  if ( index == 24 ) 
  {
    error = SENSOR_ERROR_OK;
    ProcessCse7766Packet();
    index = 0;
  }

  // Report error state (LED) of cse7766.
  if ( error == SENSOR_ERROR_OK )
    Blynk.virtualWrite( V18, 0 );
  else
    Blynk.virtualWrite( V18, 255 );
}
Posted in arduino, iot | Tagged , , | 4 Comments