1°/ program structure

  • an arduino program is called a sketch.
  • a sketch contains at least 2 functions :
    • void setup() : used to initialize global variables, serial communications, sensors, wifi connection, ... It is executed only once, just after the boot sequence.
    • void loop() : the execution entry point. As soon as it terminates, it starts again. So loop() is called endlessly, until the micro-controller crashes, resets, shutdown, ...
  • obviously, the developer can create other functions.
  • As in a C program, variables can be global or local to functions.
  • Generally, global variables are declared at the top of the sketch, after the #include and #define.

2°/ reading/writing on GPIO pins

  • a micro-controller (µC) has several GPIO (General Purpose Input/Output) pins, that can be connected to other electronic parts/devices.
  • The number of GPIOs depends on the type of µC.
  • Moreover, not all these pins are usable on a development board.
  • For example, an esp8266 has 18 GPIOs, but on most dev. boards, GPIO from 6 to 11 are not usable. But there may be other pins that are not usable on some board of a specific builder.
  • This is why boards are not totally exchangeable even if they host the same µC:
    • available GPIOs may be not the same,
    • the physical organisation of the pins on the board may greatly vary.

 

  • According to their name, these pins can be used as an input or an output. But there are some exceptions !
  • These pins can be digital or analog:
    • digital : allows to read/write a digital signal, i.e. a 2-states HIGH/LOW signal. HIGH corresponds to the high level voltage (3.3V on esp8266, 5V on ATmega328), and LOW to 0V.
    • analog : allows to read/write a signal that can vary between LOW and HIGH voltages. This variation is not continuous, but discretized over 1023/4096/... steps (depends on the µC).
  • For "electronic" reasons, the capacity to read or write on an analog pin is physically fixed: it reads or writes, but not both mode.
  • For a digital pin, we can set the mode via a function call.
  • esp8266 boards generally provide a single input analog pin and no output analog pin. esp32 boards generally provide 3 or 4 analog inputs, and 2 analog outputs.

 

  • In order to simplify the µC programming, sketches do not use the real pin number on the µC.
  • We use GPIO number or surnames, which are generally the same for all board builders.
  • For example, if you read D3 for a pin of an esp8266 board, you are almost sure that it corresponds to the GPIO 0.
  • If you read 12 for a pin of an esp32 board, you are almost sure that it corresponds to the GPIO 12.

 

  • To setup the digital pin mode, we call pinMode().
  • If the pin never changes of mode during the sketch execution, we set the mode in the setup() function. But, if its role changes (NB: pretty rare), we can also use pinMode() in loop().
  • Exemple :
void setup() {
  pinMode(D3, OUTPUT);
  pinMode(D2, INPUT);
  ...
}

 

  • reading/writing on the pin depends on its type.
  • Exemple :
int val = analogRead(A0);
byte b = digitalRead(D2);
...
analogWrite(25, 1234); // write 1234 on pin 25 of an esp32, which corresponds to 3.3*1234/4096 = 0.9942V
digitalWrite(D3,HIGH); // on an esp8266, D3 now outputs 3.3V
digitalWrite(D3,LOW); // on an esp8266, D3 now outputs 0V

 

3°/ Serial communication with a PC

  • Most of dev. boards host a chipset that allows to send a program to the µC through an USB cable.
  • In fact, this chipset allows to establish a serial connection between a PC and the µC. So it can be used to send or receive any data to/from the µC.
  • Most often, the connection is used to debug our programs by printing messages, that will be send through the serial connection, and thus that can be displayed on the PC.
  • Exemple :
...
void setup() {
  Serial.begin(115200); // initialize serial connection at 115200 bauds
  Serial.println("hello");
  ...
}

 

  • The speed of the connection must be chosen according to the capacities of the µC.
  • For example, ATmega328 (on arduino boards) allows only 9600 bauds.

 

4°/ Interrupts

  • Some µC are able to monitor the state of some GPIOs (CAUTION : only digital ones) and check if there are some changes.
  • In that case, they can interrupt what they were doing and call a function (i.e. a callback function) that must process the change event.
  • It is particularly useful to react to push on a button/switch/... because it avoids to regularly check the state of this element within the loop() function.

Exemple : react to a change on pin D3 of an esp8266 board.

volatile byte state;
...
void ICACHE_RAM_ATTR mycallback() {
  state = digitalRead(D3);
  ...
}
...
void setup() {
  ...
  attachInterrupt(digitalPinToInterrupt(D3), mycallback, CHANGE);
  ...
}
Remarks
  • There are 2 constraints to write a callback :
    • we must enforce the compiler to create a code where the callback function will be placed in RAM memory (and not in the cache) : use ICACHE_RAM_ATTR macro in the function header.
    • global variables manipulated by the callback must also stay in RAM memory (and not in cache/registers) : use volatile keyword.
  • The name of the callback function is free, provided you use the same name in attachInterrupt().
  • CHANGE will detect a state change, i.e. going from LOW to HIGH or the opposite. RISING will detect only a change from LOW to HIGH, and FALLING will detect a change from HIGH to LOW.
  • digitalPinToInterrupt() is used to "convert" the name of a GPIO into a number that is proper to the µC interrupts numbering (NB: this numbering imay be different from the associated GPIO number).
 
5°/ Pulse Width Modulation (PWM)
  • PWM is a principle that uses a digital pin to emit a signal that is constituted of pulses more or less large.
  • During a pulse, the signal level is high, and low the rest of the time.
  • The ratio between the time passed at low and high level is called the duty cycle.
  • The other parameter of PWM is the "frequency" of the signal: considering the time taken by a single low level+high level, we obtain the period of the signal and thus the frequency.
  • For exemple, if the PWM frequency is 100Hz, the period is 10ms. Then, if the duty cycle is :
    • 10% : the signal is low during 9ms then high during 1ms.
    • 50% : the signal is low during 5ms, then high during 5ms.
    • 80% : the signal is low during 2ms, then high during 8ms.
  • This principle is used to power up a device like a led, a motor, ... so that a human sees a continuous phenomena while it is physically discretized.
  • It leads to regulate the brightness of a led or the rotation speed of a motor.

 

  • Depending on the µC, there are more or less digital pins that are able to produce a PWM signal (NB: nearly all on an esp8266)
  • CAUTION : 3 functions are used to set up PWM and their names are all starting with analog.... It is just because PWM "simulate" an kind of analog signal, but it is really done with digital pins. In fact, analog pins are never able to produce such a signal.
  • analogWriteRange(range) : used to set the range of the duty cycle. It defines the number of steps we can further use to setup the duty cycle value (without counting duty cycle at 0%). For exemple, if the range is 2, possible duty cycles are 0%, 50% and 100%. If the range is 100, we can set the duty cycle close to 1%.
  • analogWriteFreq(freq) : used to set the frequency of the signal. Possible values depend on the µC and the duty cycle range (i.e. the lower the range, the higher frequency we can reach). For example, on an esp8266, frequency goes from 100Hz to about 40KHz.
  • analogWrite(pin, dc) : starts the PWM on the pin given as a parameter, with dc as a value of duty cycle. Obviously, dc must be in the range specified before.

Exemple :

analogWriteRange(100);
analogWirteFreq(10000); // 10KHz
analogWrite(D3,10); // duty cycle of 10%

 


Interlude about power consumption

  • Using wifi (and/or bluetooth) on a µC is the main source of power consumption.
  • For example, an ESP32 sending over wifi draws up to 250mA from the power supply, and about 100mA while receiving
  • If Wifi is active but not communicating, it is around 80-100mA.
  • This is huge and constitue a real problem if the µC is powered by batteries. For example, a 1000mAh battery is able to supply 100mA for 10 hours.
  • So it is important to lower the power consumption.
  • The first strategy implemented in esp32/8266 is to regularly put the wifi in an inactive state, which is called the modem sleep mode. This is the default operation mode, so there is no instruction to execute to go into this mode.
  • The duration of the sleep is determined by the interval between 2 DTIM emitted by the Wifi access point. It is generally between 100 and 1000ms.
  • In this mode, the power consumption falls down to 20mA, except when the wifi is active and communicating, for which it is still 250mA. Thus, if the µC does not communicate continuously, it is an interesting mode.
  • The drawback of this mode is that the wifi is not always active so there is a bigger latency to receive messages.
  • When the µC communicates rarely, it is possible to save much more power, by entering light-sleep mode, deep-sleep mode or even hibernation.
  • The difference between these 3 modes is that there are more or less parts of the µC that are inactive and the awakening process is different :
    • light-sleep : the CPU is paused and awakes after a predetermined amount of time. The state of the processor is stored in the RAM, which is still powered. So application data are not lost too during the sleep. The consumption falls to 1mA but the µC is unable to receive messages during the sleep, so if someone sends something, it will be lost. 
    • deep-sleep : the CPU and the RAM is stopped and only RTC (realtime clock) circuits are active. So all application data are lost (NB : it is still possible to save few values in RTC RAM that is still powered).  It awakes after a predetermined amount of time or with an external signal on a GPIO. When awakening, it boots once again. While in deep-sleep, it draws about 10µA
    • hibernation : all is off and it can be awakened only by an eternal signal. In this mode, it draws about 2µA.
  • Caution : all the current consumptions are given only for the µC. The real consumption is higher because of all chipsets that are used on the PCB around the µC (voltage regulator, usb-serial converter, ...). For example, it is quite common to obtain a real consumption around 50µA in deep-sleep.

 

  • Apart these modes, it is also possible the switch off wifi when it is not needed : WiFi.mode(WIFI_OFF)
  • It is also possible to set wifi always active, in case of small latency is needed : WiFi.setSleepMode(WIFI_NONE_SLEEP)

 6°/ Wifi

  • esp8266 and esp32 have native wifi support, which is not the case of ATmega328
  • An esp8266/esp32 can be used as a wifi client, a wifi access point, or both.
  • A client just connects to an access point and receives an IP address.
  • If a µC is used has an access point, it provides wifi connections and IP addresses (like a DHCP server), but it has not itself an IP address.
  • Nevertheless, a µC can be an access point AND a client at a time.
  • Whatever the case, a sketch replying on Wifi must include an header file that contains the wifi function declarations.
  • For arduino/esp32 board, it is WiFi.h, and for esp8266 boards, it is ESP8266WiFi.h


6.1°/ µC as a Wifi client

  • The "common way" is to create a function that is called in setup().
  • this function tries endlessly to establish the Wifi connection.

Exemple :

#include <ESP8266WiFi.h>
...
void setupWifi() {
     
  const char *ssid = "ssid_wifi_ap"; // the ssid of the AP        
  const char *password = "pass_ap"; // the password of the AP  
  
  WiFi.setAutoConnect(false);  // see comments   
  ret = WiFi.setSleepMode(WIFI_NONE_SLEEP); // always active => big consumption

  ret = WiFi.mode (WIFI_STA); // setup as a wifi client
  ret = WiFi.begin(ssid,password); // try to connect
  while (WiFi.status() != WL_CONNECTED) { // check connection
    delay(500); 
    Serial.print(".");
  } 
  // debug messages
  Serial.print("Connected, IP address: ");
  Serial.println(WiFi.localIP());
}

 

Remarks : 

  • setAutoConnect() is used to prevent the µC to automatically establish a connection with the last know AP (its credentials are kept in a king of flash memory).
  • By default, this automatic connection occurs just after the boot sequence, and it may prevent the µC to connect to the ssid given in the code if it differs from the last one used.
  • This is why it is a good idea to disable this automatic connection, unless you are sure that your µC will always connect to the same ssid.

 

6.2°/ µC as a Wifi access point+client

  •  Since the AP will act as a DHCP server, we must set variables to configure the network address, the network mask, ...
  • In the following example, we assume that the AP will provide addresses like 192.168.0.XXX
  • We also must choose a wifi channel for the Wifi network.
  • Note that the Wifi norm defines 13 channels that correspond to 13 frequencies around 2.4GHz. The problem is that 2 wifi networks cannot use channels that are too close unless it produces a lot of interference, and thus a lot of packet loss. The least gap is 4.
  • An esp8266/esp32 is not really powerful, so the maximum number of clients is 4 by default. Moreover, if it is itself a client, it has in charge the AP + its own code, it may rapidly crashes if it is overloaded. So, for a reliable solution, it is better to use a dedicated AP. But if we just want to communicate between 2 µC, it is sufficient and simple.
#include <ESP8266WiFi.h>
...
void setupAP() {
  IPAddress local_IP(192,168,0,1);
  IPAddress gateway(192,168,0,1);
  IPAddress subnet(255,255,255,0);

  ret = WiFi.mode(WIFI_AP_STA); // AP + client
  int channel = 1; // choose between 1 and 13
  const char *ssid = "ssid_wifi_ap";        
  const char *password = "pass_ap";  

 // CAUTION: on other µC, 2 following lines may be exchanged to work
  ret = WiFi.softAP(ssid, password, channel); 
  ret = WiFi.softAPConfig(local_IP, gateway, subnet);

  // debug messages
  IPAddress myIP = WiFi.softAPIP();
  Serial.print("AP IP address: ");
  Serial.println(myIP);
}

 

7°/ TCP connection & communication

  • Once a Wifi connection is established and the µC has an IP address, it is possible to create a TCP connection.
  • A µC can act as a TCP client, or a TCP server.
  • When the TCP connection is established, we can communicate by sending/receiving bytes.

 

7.1°/ µC as a TCP Client

  • Once again, we use a dedicated function to establish the connection with the server.
  • Generally, it is a good idea to use a variable that store the state of this connection. 

Exemple :

#include <ESP8266WiFi.h>
...
WiFiClient client;
byte connState;
const char* ipServ = "192.168.0.1";
int portServ = 12345;
byte buf[1024];
int idx = 0;
...

void setup() {
  Serial.begin(115200);
  connState = 0;
  // establish the wifi connection
  ...
}

bool doConnection() {
  if (client.connect(ipServ, portServ)) {
    connState = 1;
    Serial.println("connected to server")
    // special for esp8266: disable buffering small mesg
    client.setNoDelay(true);
    return true;
  }
  else {
    connState = 0;
    Serial.println("connection failed")
    return false;
}

void loop() {
  if (connState == 0) {
    if (! doConnection()) { delay(1000); }
  }
  if (connState == 1) {
    client.write(10); // write a byte equal to 10
    client.println("10"); // write 3 bytes: ascii code of 1, ascii code of 0, ascii code of new_line
    // wait something from the server
    while (!client.available()) {} // see comments below
    // read what is available
    while (client.available()) {
      buf[idx++] = client.read(); // read & store a byte
    }
    Serial.println(String(buf)); // print the content of buf as a string
    for(int i=0;i<idx;i++) buf[i] = 0; // reset buf
  }
}

 

Comments :

  • reading from a WiFiClient is tedious because there is only one function : read(), that reads just a byte. Moreover, it is not blocking (which is not the case for classical processors with an OS like unix)
  • Nevertheless, it is possible to obtain a blocking reception using : while(!client.available()) {}. On a µC, it works perfectly while on a classical processor, it is forbidden because it loads it at 100%.

7.2°/ µC as a TCP server

Exemple :

#include <ESP8266WiFi.h>
...
WiFiServer server(12345);
WiFiClient client;
byte connState;
byte buf[1024];
int idx = 0;
...

void setup() {
  Serial.begin(115200);
  connState = 0;
  // establish the wifi connection
  ...
}

void waitConnection() {
  client = server.available(); // test if there is an incoming connection and if yes, accept it.
  if (client) {
    connState = 1;
    Serial.println("client connected")
    // special for esp8266: disable buffering small mesg
    client.setNoDelay(true);
  }
}

void loop() {
  if (connState == 0) {
    waitConnection();
  }
  if (connState == 1) {
    // wait something from the client
    while (!client.available()) {}
    // read what is available
    while (client.available()) {
      buf[idx++] = client.read(); // read & store a byte
    }
    Serial.println(String(buf)); // print the content of buf as a string
    for(int i=0;i<idx;i++) buf[i] = 0; // reset buf
  }
}