I'm currently trying to use ESP-NOW to transmit a 100hz square wave. I'm converting the square wave into data using an ADC pin and attempting to achieve real-time transmission using ESP-NOW. The problem is that the receiver cannot perfectly receive the data sent by the transmitter. Does anyone have any suggestions for this?
The ESP can't simultaneously sample an input and transmit data, so the transmitted result is the input, but sampled slowly and unevenly.
Look up "sampling theorem" and "signal aliasing".
To get a reliable picture of the data, sample and store the result in buffer, using a sample rate at least 10x higher than the square wave frequency, then transmit the buffer.
You sample analog signals and transmit the result without controlling and checking the time between two consecutive measurements.
TCP is a protocol that - for good reasons - does not rely on synchronisation of sender/receiver and it is package based (sends a number of data in one package):
To correctly reconstruct the signal the receiver must have knowledge of the sample time(s) ...
Also see @jremington's post regarding sampling requirements and either use a fixed sampling rate known at the receiver side or you have to transmit a structure of sampling data and timing interval. With this you should be able to properly reconstruct the wave form.
[Edit]: I must correct that - while you are using ESP NOW - the TCP protocol does not apply.
ESP NOW provides a vendor-specific action frame with the format:
-------------------------------------------------------------------------------
| Element ID | Length | Organization Identifier | Type | Version | Body |
-------------------------------------------------------------------------------
1 byte 1 byte 3 bytes 1 byte 1 byte 0~250 bytes
Each frame can transport a payload of 0 to 250 bytes (see the table above).
A complete frame consists of
in minimum (zero payload) -> 43 bytes
with payload of 2 bytes/characters -> 45 bytes -> 360 bits
There are still the following observations:
As radio transmission is depending on the availability of the used channel you cannot be sure that the timing of the transmission is synchronized with the sampling rate.
If you sample with a given rate the receiver can restore the signal after reception. Use a timer function (or at least every x microseconds in loop() ) to sample the data.
Single frames may not be received due to RF interference. The receiver cannot detect whether one or more frames/data are missing.
If you integrate a package counter into each frame the receiver can check whether data are missing of not. You have to decide on an error handling if that applies.
You transmit a frame of 45 bytes for a payload of 2 bytes.
It would be more efficient to collect e.g. 100 samples and transmit them with one frame.
You use one character for the data read by analogRead(). As analogRead() has as a standard 12-bit resolution with an ESP32 the data are 0 ... 4095 which requires two bytes.
Your sketch only works like it does at the moment because you are only interested in HIGH/LOW and not the real amplitude. If this is the case you could code the rectangle in binary data, each bit representing a certain time interval and the status HIGH or LOW. That would drop the required data from 1 byte per sample to 1 bit per sample (8 times reduction).
To add more to the sampling problem. The sampling MUST be more that twice the shortest time change in your signal. What time change will that be? That will be the rise time and the decay time for the edges of your square wave. What is that time? You will need a good oscilloscope to determine that.
In fact, I am currently studying this post, and if I can use the code in this article, I believe it can solve my problem. However, when I use his code, I cannot measure the waveform on the receivier's pin_DAC. Can someone help me take a look at the code in this article to see if there are any problem ? Thank you all."
#define CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED 0
#define CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED 0
#include <esp_now.h>
#include <esp_wifi.h>
#include <driver/dac.h>
// Structure example to receive data
// Must match the sender structure
uint8_t IN_BUFF[250];
uint8_t OUT_BUFF[250];
volatile uint8_t i=0;
volatile uint8_t data4dac=0;
hw_timer_t *My_timer = NULL;
// callback function that will be executed when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
//Copying received bytes to buffer and setting flag for new data for the DAC
memcpy(IN_BUFF, incomingData, sizeof(IN_BUFF));
data4dac=1;
}
void IRAM_ATTR writeDAC(){
//If there is new data for the DAC
if(data4dac){
if(i==0){
//If we finished sending previous 250 bytes, copy new ones from IN_BUFF to OUT_BUFF
memcpy(OUT_BUFF, IN_BUFF, sizeof(OUT_BUFF));
}
dac_output_voltage(DAC_CHANNEL_1, OUT_BUFF[i++]);
if(i==250){
//If we reached end of 250 bytes, reset counter and data4dac flag
i=0;
data4dac=0;
}
}
}
void setup() {
// Initialize Serial Monitor
Serial.begin(115200);
dac_output_enable(DAC_CHANNEL_1);
// Set device as a Wi-Fi Station
esp_netif_init();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_bandwidth(WIFI_IF_STA, WIFI_BW_HT40);
esp_wifi_set_storage(WIFI_STORAGE_RAM);
esp_wifi_set_ps(WIFI_PS_NONE);
esp_wifi_start();
// Init ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_wifi_config_espnow_rate(WIFI_IF_STA, WIFI_PHY_RATE_54M);
//Setting up a timer interrupt with a freq. of 20kHz
My_timer = timerBegin(0, 8, true);
timerAttachInterrupt(My_timer, &writeDAC, true);
timerAlarmWrite(My_timer, 500, true);
timerAlarmEnable(My_timer);
esp_now_register_recv_cb(OnDataRecv);
}
void loop() {
}
The code you found solves a lot of the observations:
The replay rate at the receiver is identical to the sample rate at the transmitter.
The transmitter uses the full payload of one ESP NOW frame.
This routine in the transmitter sketch
void IRAM_ATTR sampleADC(){
//Send a new sine wave byte to DAC1
dac_output_voltage(DAC_CHANNEL_1, IN_BUFF[i++]);
if(i==250){
//If all 250 bytes have been sent, reset counter and prepare esp-now send if previous esp-now frame has been already sent out.
i=0;
if(sent){
memcpy(OUT_BUFF, IN_BUFF, 250);
dataReady=1;
sent=0;
}
}
}
seems to be wrong as dac_output_voltage(DAC_CHANNEL_1, IN_BUFF[i++]); does not read from ADC but sets the output channel to IN_BUFF[i++].
Is it because both the transmitter and receiver use the same interrupt frequency to minimize data loss during transmission?
I believe the output from the transmitter's DAC should be fine because I can measure sine wave on the oscilloscope, but I cannot measure it at the receiver.
Yes: The same interrupt rate for sampling and replay is required to reconstruct the signal with correct timing.
No: It does not help in case of data loss. As all data are sent as independent frames you have to take care of that on your own.
(When you use the internet the TCP stack (software) takes care that packages arriving in a different order than they have been sent are rearranged in the correct sequence before handed over to the user application.)
I have not checked the complete sketch regarding its function, but on a first glance it looks as if this is done by the transmitter sketch:
IN_BUF[250] is preset with certain data
In the timer routine sampleADC() :
The byte IN_BUF[i]is replayed, the index i is incremented
If all 250 bytes have been replayed (i == 250)
i is set to zero
if the previous OUT_BUF was sent (sent ==1)
the data from IN_BUF are copied to OUT_BUF
the variable dataReady is set to 1
The variable "sent" makes sure that IN_BUF is only copied to OUT_BUF if the previous data have been sent.
The variable "dataReady" signalizes to the function in loop() that new data are ready for transmission.
If I do not miss anything in the code, IN_BUF is never changed and therefore always the same data as in the declaration of IN_BUF should be transmitted to the receiver and are replayed there via DAC:
You may try this modified receiver sketch to print the block of the first 250 data received:
Modified Receiver Sketch
/*
Modified receiver sketch from
Forum: https://forum.arduino.cc/t/transmitting-a-square-wave-through-esp-now/1165489/17
Should print the first 250 data received
*/
#define CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED 0
#define CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED 0
#include <esp_now.h>
#include <esp_wifi.h>
#include <driver/dac.h>
// Structure example to receive data
// Must match the sender structure
uint8_t IN_BUFF[250];
uint8_t OUT_BUFF[250];
uint8_t PRT_BUFF[250];
volatile uint8_t i=0;
volatile uint8_t data4dac=0;
volatile uint8_t data2print =0;
hw_timer_t *My_timer = NULL;
// callback function that will be executed when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
//Copying received bytes to buffer and setting flag for new data for the DAC
memcpy(IN_BUFF, incomingData, sizeof(IN_BUFF));
data4dac=1;
data2print=1;
}
void IRAM_ATTR writeDAC(){
//If there is new data for the DAC
if(data4dac){
if(i==0){
//If we finished sending previous 250 bytes, copy new ones from IN_BUFF to OUT_BUFF
memcpy(OUT_BUFF, IN_BUFF, sizeof(OUT_BUFF));
}
dac_output_voltage(DAC_CHANNEL_1, OUT_BUFF[i++]);
if(i==250){
//If we reached end of 250 bytes, reset counter and data4dac flag
i=0;
data4dac=0;
}
}
}
void setup() {
// Initialize Serial Monitor
Serial.begin(115200);
Serial.println("Setup()");
dac_output_enable(DAC_CHANNEL_1);
// Set device as a Wi-Fi Station
esp_netif_init();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_bandwidth(WIFI_IF_STA, WIFI_BW_HT40);
esp_wifi_set_storage(WIFI_STORAGE_RAM);
esp_wifi_set_ps(WIFI_PS_NONE);
esp_wifi_start();
// Init ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_wifi_config_espnow_rate(WIFI_IF_STA, WIFI_PHY_RATE_54M);
//Setting up a timer interrupt with a freq. of 20kHz
My_timer = timerBegin(0, 8, true);
timerAttachInterrupt(My_timer, &writeDAC, true);
timerAlarmWrite(My_timer, 500, true);
timerAlarmEnable(My_timer);
esp_now_register_recv_cb(OnDataRecv);
Serial.println("Entering Loop()");
}
uint8_t printDone = 1;
void loop() {
if (data2print && printDone) {
printDone = 0;
data2print = 0;
memcpy(PRT_BUFF, IN_BUFF, sizeof(PRT_BUFF));
}
prt();
}
void prt(){
while (!printDone) {
Serial.println("------------------------------------------------------------------");
for (int i=0;i<sizeof(PRT_BUFF);i++){
Serial.print(PRT_BUFF[i]);
Serial.print('\t');
if (!((i+1) % 8)) {
Serial.println();
};
}
Serial.println();
Serial.println("------------------------------------------------------------------");
printDone = 1;
}
}
To sample from main, I used a task with IRAM_ATTR and the code below. With the CPU @ 80MHz (for other reasons..) and normal debugging it then achieves about 18.5 kHz with ADC1 and 21.0 kHz with ADC2 (both ADCs at 12bit).
Disabling the assertion/debug level and set the compiler optimizations it improves to 20.4 kHz on ADC1 and 23.3 kHz on ADC2. (sdkconfig: CONFIG_OPTIMIZATION_LEVEL_RELEASE=y and CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED=y ).
So it looks as if the 20KHz reading as done in your sketch is only achievable with optimizations.
I recommend to use this timer configuration for the transmitter:
With a value of 1000 im timerAlarmWrite() you get 1 kHz sample rate which is 10 times your intended 100 Hz signal.
The Timer routine to sample could be modified as follows:
constexpr byte analogPin = 36;
void IRAM_ATTR sampleADC(){
IN_BUFF[i++] = byte(analogRead(analogPin) >> 4) ; // To map from 4095 to 255
if(i==250){
//If all 250 bytes have been sent, reset counter and prepare esp-now send if previous esp-now frame has been already sent out.
i=0;
if(sent){
memcpy(OUT_BUFF, IN_BUFF, 250);
dataReady=1;
sent=0;
}
}
}
You have to use the same configuration of course also for the receiver timer:
Have you considered something off the shelf that takes care of the timing, error detection and runs in the background without Arduino attention? If this sounds interesting look at CAN, I believe it will do exactly what you want without all the code overhead.