PWM with Arduino and ESP32

analogWrite

for Arduino
Syntax

analogWrite(pin, value)

Parameters
pin: the Arduino pin to write to. Allowed data types: int.
value: the duty cycle: between 0 (always off) and 255 (always on). Allowed data types: int.

TONE

For Arduino
Generates a square wave of the specified frequency (and 50% duty cycle) on a pin. A duration can be specified, otherwise the wave continues until a call to noTone(). The pin can be connected to a piezo buzzer or other speaker to play tones.

tone(pin, frequency)
tone(pin, frequency, duration)
noTone(pin)

LEDC

The ESP32 has a LED PWM controller with 16 independent channels that can be configured to generate PWM signals with different properties.

Here’s the steps you’ll have to follow to dim an LED with PWM using the Arduino IDE:

1. First, you need to choose a PWM channel. There are 16 channels from 0 to 15.

2. Then, you need to set the PWM signal frequency. For an LED, a frequency of 5000 Hz is fine to use.

3. You also need to set the signal’s duty cycle resolution: you have resolutions from 1 to 16 bits.  We’ll use 8-bit resolution, which means you can control the LED brightness using a value from 0 to 255.

4. Next, you need to specify to which GPIO or GPIOs the signal will appear upon. For that you’ll use the following function:

ledcAttachPin(GPIO, channel)

This function accepts two arguments. The first is the GPIO that will output the signal, and the second is the channel that will generate the signal.

5. Finally, to control the LED brightness using PWM, you use the following function:

ledcWrite(channel, dutycycle)

This function accepts as arguments the channel that is generating the PWM signal, and the duty cycle.

// the number of the LED pin
const int ledPin = 16;  // 16 corresponds to GPIO16

// setting PWM properties
const int freq = 5000;
const int ledChannel = 0;
const int resolution = 8;
 
void setup(){
  // configure LED PWM functionalitites
  ledcSetup(ledChannel, freq, resolution);
  
  // attach the channel to the GPIO to be controlled
  ledcAttachPin(ledPin, ledChannel);
}
 
void loop(){
  // increase the LED brightness
  for(int dutyCycle = 0; dutyCycle <= 255; dutyCycle++){   
    // changing the LED brightness with PWM
    ledcWrite(ledChannel, dutyCycle);
    delay(15);
  }

  // decrease the LED brightness
  for(int dutyCycle = 255; dutyCycle >= 0; dutyCycle--){
    // changing the LED brightness with PWM
    ledcWrite(ledChannel, dutyCycle);   
    delay(15);
  }
}

 to set the frequency again, we call the ledcWriteTone function, passing as inputs the PWM channel and the frequency to set. We will set it to 2000 Hz, as the initial configuration.

ledcWriteTone(channel, 2000);

Arduino and Esp32 Interrupts (ISR)

Attaching Interrupt to a GPIO Pin

In Arduino IDE, we use a function called attachInterrupt() to set an interrupt on a pin by pin basis. The recommended syntax looks like below.

attachInterrupt(GPIOPin, ISR, Mode);

This function takes three parameters:

GPIOPin – Sets the GPIO pin as an interrupt pin, which tells the ESP32 which pin to monitor.

ISR – Is the name of the function that will be called every time the interrupt is triggered.

Mode – Defines when the interrupt should be triggered. Five constants are predefined as valid values:

LOWTriggers interrupt whenever the pin is LOW
HIGHTriggers interrupt whenever the pin is HIGH
CHANGETriggers interrupt whenever the pin changes value, from HIGH to LOW or LOW to HIGH
FALLINGTriggers interrupt when the pin goes from HIGH to LOW
RISINGTriggers interrupt when the pin goes from LOW to HIGH

Detaching Interrupt from a GPIO Pin

You can optionally call detachInterrupt() function when you no longer want ESP32 to monitor a pin. The recommended syntax looks like below.

detachInterrupt(GPIOPin);

Interrupt Service Routine

Interrupt Service Routine is invoked when an interrupt occurs on any GPIO pin. Its syntax looks like below.

void IRAM_ATTR ISR() {
    Statements;
}

ISRs in ESP32 are special kinds of functions that have some unique rules most other functions do not have.

  • The interrupt service routine must have an execution time as short as possible, because it blocks the normal program execution.
  • Interrupt service routines should have the IRAM_ATTR attribute, according to the ESP32 documentation

What is IRAM_ATTR?

By flagging a piece of code with the IRAM_ATTR attribute we are declaring that the compiled code will be placed in the Internal RAM (IRAM) of the ESP32.
Otherwise the code is placed in the Flash. And flash on the ESP32 is much slower than internal RAM.
If the code we want to run is an interrupt service routine (ISR), we generally want to execute it as quickly as possible. If we had to ‘wait’ for an ISR to load from flash, things would go horribly wrong.

struct Button {
  const uint8_t PIN;
  uint32_t numberKeyPresses;
  bool pressed;
};

Button button1 = {18, 0, false};

void IRAM_ATTR isr() {
  button1.numberKeyPresses += 1;
  button1.pressed = true;
}

void setup() {
  Serial.begin(115200);
  pinMode(button1.PIN, INPUT_PULLUP);
  attachInterrupt(button1.PIN, isr, FALLING);
}

void loop() {
  if (button1.pressed) {
      Serial.printf("Button 1 has been pressed %u times\n", button1.numberKeyPresses);
      button1.pressed = false;
  }

  //Detach Interrupt after 1 Minute
  static uint32_t lastMillis = 0;
  if (millis() - lastMillis > 60000) {
    lastMillis = millis();
    detachInterrupt(button1.PIN);
	Serial.println("Interrupt Detached!");
  }
}

touch interrupt

int threshold = 40;
bool touch1detected = false;
bool touch2detected = false;

void gotTouch(){
 touch1detected = true;
}

void gotTouch1(){
 touch2detected = true;
}

void setup() {
  Serial.begin(115200);
  delay(1000); // give me time to bring up serial monitor
  printf("\n ESP32 Touch Interrupt Test\n");
  touchAttachInterrupt(T2, gotTouch, threshold);
  touchAttachInterrupt(T3, gotTouch1, threshold);
}

void loop(){
  if(touch1detected){
    touch1detected = false;
    Serial.println("Touch 1 detected");
  }
  if(touch2detected){
    touch2detected = false;
    Serial.println("Touch 2 detected");
  }
}

timer interrupt

#include<Arduino.h>

volatile int interrupts;
int totalInterrupts;

hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;



void IRAM_ATTR onTime() {
	portENTER_CRITICAL_ISR(&timerMux);
	interrupts++;
	portEXIT_CRITICAL_ISR(&timerMux);
}


void setup() {

	Serial.begin(9600);

	// Configure Prescaler to 80, as our timer runs @ 80Mhz
	// Giving an output of 80,000,000 / 80 = 1,000,000 ticks / second
	timer = timerBegin(0, 80, true);                
	timerAttachInterrupt(timer, &onTime, true);    
	// Fire Interrupt every 1m ticks, so 1s
	timerAlarmWrite(timer, 1000000, true);			
	timerAlarmEnable(timer);
}
void loop() {
	if (interrupts > 0) {
		portENTER_CRITICAL(&timerMux);
		interrupts--;
		portEXIT_CRITICAL(&timerMux);
		totalInterrupts++;
		Serial.print("totalInterrupts");
		Serial.println(totalInterrupts);
	}
}

note: portEnter_CRITICAL is expand for vTaskEnterCritical

Arduino AND ESP32 Wire Library

functions

begin()
Initiate the Wire library and join the I2C bus as a master or slave. This should normally be called only once.
Parameters
Arduino
address: the 7-bit slave address (optional); if not specified, join the bus as a master.
ESP32
there are no address only you can select scl gpio , sda gpio and frequency

requestFrom()
Used by the master to request bytes from a slave device.
Syntax
Wire.requestFrom(address, quantity)
Wire.requestFrom(address, quantity, stop)
Parameters
address: the 7-bit address of the device to request bytes from
quantity: the number of bytes to request
stop : boolean. true will send a stop message after the request, releasing the bus. false will continually send a restart after the request, keeping the connection active.
Returns
byte : the number of bytes returned from the slave device

beginTransmission()
Begin a transmission to the I2C slave device with the given address.
Parameters :
the 7-bit address of the device to transmit to

endTransmission()
Ends a transmission to a slave device
Syntax
Wire.endTransmission()
Wire.endTransmission(stop)
Parameters
stop : boolean. true will send a stop message, releasing the bus after transmission. false will send a restart, keeping the connection active.
Returns
byte, which indicates the status of the transmission:

  • 0:success
  • 1:data too long to fit in transmit buffer
  • 2:received NACK on transmit of address
  • 3:received NACK on transmit of data
  • 4:other error

write()
Writes data from a slave device in response to a request from a master, or queues bytes for transmission from a master to slave device (in-between calls to beginTransmission() and endTransmission()).
Syntax
Wire.write(value)
Wire.write(string)
Wire.write(data, length)
Parameters
value: a value to send as a single byte
string: a string to send as a series of bytes
data: an array of data to send as bytes
length: the number of bytes to transmit
Returns
byte: write() will return the number of bytes written, though reading that number is optional

available()
Returns the number of bytes available for retrieval with read(). This should be called on a master device after a call to requestFrom() or on a slave inside the onReceive() handler.
Returns
The number of bytes available for reading.

read()
Reads a byte that was transmitted from a slave device to a master after a call to requestFrom() or was transmitted from a master to a slave.
Returns
The next byte received

SetClock()
This function modifies the clock frequency for I2C communication. I2C slave devices have no minimum working clock frequency, however 100KHz is usually the baseline.
Syntax
Wire.setClock(clockFrequency)
Parameters
clockFrequency: the value (in Hertz) of desired communication clock. Accepted values are 100000 (standard mode) and 400000 (fast mode). Some processors also support 10000 (low speed mode), 1000000 (fast mode plus) and 3400000 (high speed mode). Please refer to the specific processor documentation to make sure the desired mode is supported.

onReceive()
Registers a function to be called when a slave device receives a transmission from a master.
Parameters
handler: the function to be called when the slave receives data; this should take a single int parameter (the number of bytes read from the master) and return nothing, e.g.: void myHandler(int numBytes)

onRequest()
Register a function to be called when a master requests data from this slave device.
Parameters
handler: the function to be called, takes no parameters and returns nothing

ESP32

in esp32 you can create more than one i2c bus by Create Object from TwoWire instead of using public general object Wire

The ESP32 supports I2C communication through its two I2C bus interfaces that can serve as I2C master or slave, depending on the user’s configuration. Accordingly to the ESP32 datasheet, the I2C interfaces of the ESP32 supports:

  • Standard mode (100 Kbit/s) 
  • Fast mode (400 Kbit/s) 
  • Up to 5 MHz, yet constrained by SDA pull-up strength 
  • 7-bit/10-bit addressing mode 
  • Dual addressing mode. Users can program command registers to control I²C interfaces, so that they have more flexibility

The SDA and SCL lines are active low, so they should be pulled up with resistors. Typical values are 4.7k Ohm for 5V devices and 2.4k Ohm for 3.3V devices.

Most sensors we use in our projects are breakout boards that already have the resistors built-in. So, usually, when you’re dealing with this type of electronics components you don’t need to worry about this.

Connecting an I2C device to an ESP32 is normally as simple as connecting GND to GND, SDA to SDA, SCL to SCL and a positive power supply to a peripheral, usually 3.3V (but it depends on the module you’re using).

Examples

master esp32

#include <Arduino.h>
#include <Wire.h>


TwoWire i2c(0);//you can use general Wire object or create one by TwoWire

void setup(){
  Serial.begin(9600);
  i2c.begin(33,32,8000000);
}

void loop(){
  i2c.beginTransmission(4);
  i2c.write("hello there");
  Serial.println("send data");
  i2c.endTransmission();
  delay(1000);
  i2c.requestFrom(4,10,true);// true for stop connection for can use beginTransmission next time
  while(i2c.available())    // slave may send less than requested
  { 
    char c = i2c.read(); // receive a byte as character
    Serial.print(c);         // print the character
  }
  delay(5000);

}

slave arduino

#include<Wire.h>

void receiveEvent(int coutn){
  while(0 < Wire.available()) 
  {
    char c = Wire.read(); 
    Serial.print(c);         
  }
  Serial.println("");
}

void requestEvent(int count){
  Wire.write("hi how are");
  Serial.println(String("master request ")+String(count));
}

void setup() {
  Serial.begin(9600);
  Wire.begin(4);
  
  Wire.onReceive(receiveEvent);
  Wire.onRequest(requestEvent);

}

void loop() {
  delay(100);

}

Scan available addresses

#include <Wire.h>
 
void setup() {
  Wire.begin();
  Serial.begin(115200);
  Serial.println("\nI2C Scanner");
}
 
void loop() {
  byte error, address;
  int nDevices;
  Serial.println("Scanning...");
  nDevices = 0;
  for(address = 1; address < 127; address++ ) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0) {
      Serial.print("I2C device found at address 0x");
      if (address<16) {
        Serial.print("0");
      }
      Serial.println(address,HEX);
      nDevices++;
    }
    else if (error==4) {
      Serial.print("Unknow error at address 0x");
      if (address<16) {
        Serial.print("0");
      }
      Serial.println(address,HEX);
    }    
  }
  if (nDevices == 0) {
    Serial.println("No I2C devices found\n");
  }
  else {
    Serial.println("done\n");
  }
  delay(5000);          
}
Tagged : / /