Utilisation de la puce mémoire 24CSM01 (et une autre chose !) avec le framework ESP-IDF – Partie 2.

Posted on mer. 22 janvier 2025 in Electronics

Dans le post précédent, j’ai commencé à décrire le fonctionnement du framework ESP-IDF et de son utilisation pour piloter le bus I2C. Comme promis, nous allons voir comment ajouter un second périphérique sur le bus et y écrire des données.

Ajouter un second périphérique sur le bus I2C

Je vais « tricher » un peu : ce second périphérique est en fait le registre de configuration de la 24CSM01. Comme il dispose de sa propre adresse sur le bus I2C, il est à considérer comme un périphérique à part d’un point de vue du bus. Allons-y, intégrons notre nouveau périphérique.

#include <stdio.h>
#include "stdint.h"
#include "driver/i2c_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define CNFREGADDR 0b1011000
#define CNFREGWA0 0b10001000
#define CNFREGWA1 0b00000000
#define LOCK 0x99
#define NOLOCK 0x66
#define LENGTH 48

i2c_master_bus_config_t config = {
    .clk_source = I2C_CLK_SRC_DEFAULT,
    .i2c_port = I2C_NUM_0,
    .scl_io_num = 22,
    .sda_io_num = 21,
    .glitch_ignore_cnt = 7,
    .flags.enable_internal_pullup = true,
};

i2c_master_bus_handle_t hi2c0;

i2c_device_config_t devconfig = {
    .dev_addr_length = I2C_ADDR_BIT_LEN_7,
    .device_address = CNFREGADDR,
    .scl_speed_hz = 100000,
};
i2c_device_config_t eepromconfig = {
    .dev_addr_length = I2C_ADDR_BIT_LEN_7,
    .device_address = 0b1010000,
    .scl_speed_hz = 100000,
};
i2c_master_dev_handle_t confreg_handle;
i2c_master_dev_handle_t eeprom_handle;

static void disp_buf(uint8_t *buf, int len)
{
    int i;
    for (i = 0; i < len; i++) {
        printf("%02x (%b)", buf[i], buf[i]);
        if ((i + 1) % 16 == 0) {
            printf("\n");
        }
    }
    printf("\n");
}

void app_main(void)
{
    uint8_t buffer[LENGTH] = {CNFREGWA0, CNFREGWA1};
    uint8_t read_buffer[LENGTH];
    i2c_new_master_bus(&config, &hi2c0);
    i2c_master_bus_add_device(hi2c0, &devconfig, &confreg_handle);
    i2c_master_transmit_receive(confreg_handle, buffer, 2, read_buffer, 2, -1);
    disp_buf(read_buffer, 2);
    vTaskDelay(500/portTICK_PERIOD_MS);
    printf("Now setting all memzones to zero\n");
    buffer[2] = 0; buffer[3] = 0; buffer[4] = NOLOCK;
    i2c_master_transmit(confreg_handle, buffer, 5, -1);
    printf("Set\n");
    vTaskDelay(20/portTICK_PERIOD_MS);
    i2c_master_transmit_receive(confreg_handle, buffer, 2, read_buffer, 2, -1);
    disp_buf(read_buffer, 2);

    buffer[0] = 0x00; buffer[1] = 0x01;
    i2c_master_bus_add_device(hi2c0, &eepromconfig, &eeprom_handle);
    //i2c_master_transmit(eeprom_handle, buffer, 3, -1);
    i2c_master_transmit_receive(eeprom_handle, buffer, 2, read_buffer, 3, -1);
    disp_buf(read_buffer, 3);
    vTaskDelay(1000/portTICK_PERIOD_MS);

}

Voilà. C’est plutôt simple, non ?

Je vous sens un peu sur votre faim… Et moi aussi.

Allez, on se rajoute un peu de difficulté !

Utilisation du CAN

Et si nous essayions d’écrire des données qui ont du sens dans cette EEPROM ? Je vous propose pour cela de réaliser quelques lectures avec le Convertisseur Analogique Numérique (ou ADC pour les anglophones), et d’en stocker le résultat dans la 24CSM01.

Comme pour le bus I2C, nous devons réaliser cette opération en plusieurs étapes.

Initialisation du CAN

Tout d’abord, nous devons configurer et initialiser le convertisseur et ses canaux, un peu à la manière de la configuration du bus et de ses périphériques. Pour ce post, je suis parti sur une utilisation de l’ADC en oneshot (une mesure unique), mais on peut aussi l’utiliser en mesure continue.

#include "hal/adc_types.h"
#include "esp_adc/adc_oneshot.h"

int adc_raw;
int voltage;

void app_main(void) {
    // Initialisation de l’ADC1
    adc_oneshot_unit_handle_t hadc1;
    adc_oneshot_unit_init_cfg_t adc_config = {
        .unit_id = ADC_UNIT_1,
    };
    adc_oneshot_new_unit(&adc_config, &hadc1);
    // Configuration d’un channel l’ADC1
    adc_oneshot_chan_cfg_t chan_config = {
        .atten = ADC_ATTEN_DB_12,
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    adc_oneshot_config_channel(hadc1, ADC_CHANNEL_0, &chan_config);
}

Comme pour le bus I2C, on créée un handler et une configuration pour notre ADC puis pour notre canal de mesure. La configuration de l’ADC est assez simple, puisqu’on lui dit juste quel ADC on souhaite utiliser (ici le n°1).

La fonction adc_oneshot_new_unit permet d’associer le handler et la configuration de l’ADC.

Il nous reste alors à configurer la voie de mesure. On y définit notamment la résolution de l’ADC, que j’ai laissé au maximum (ADC_BITWIDTH_DEFAULT). On pourrait cependant tout à fait utiliser une résolution inférieure si on estime ne pas avoir besoin des 12 bits de base. On y perd certes en résolution, mais on y gagne en vitesse de conversion. C’est un choix à faire.

Prise d’une mesure

// Dans app_main()
adc_oneshot_read(hadc1, ADC_CHANNEL_0, &adc_raw);

Ce petit bout de code prend une mesure sur le channel 0 et la stocke dans la variable adc_raw. Le code ci-dessous permet d’avoir un premier aperçu de ce fonctionnement, en renvoyant la valeur brute du CAN dans le terminal.

main.cpp

#include <stdio.h>
#include "stdint.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "hal/adc_types.h"
#include "esp_adc/adc_oneshot.h"

int adc_raw;
int voltage;

int calcMilliVolt(int adcValue) {
    return((3300*adcValue)/4095);
}


void app_main(void)
{
    // Initialisation de l’ADC1
    adc_oneshot_unit_handle_t hadc1;
    adc_oneshot_unit_init_cfg_t adc_config = {
        .unit_id = ADC_UNIT_1,
    };
    adc_oneshot_new_unit(&adc_config, &hadc1);
    // Configuration d’un channel l’ADC1
    adc_oneshot_chan_cfg_t chan_config = {
        .atten = ADC_ATTEN_DB_12,
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    adc_oneshot_config_channel(hadc1, ADC_CHANNEL_0, &chan_config);

    while(1) {
        adc_oneshot_read(hadc1, ADC_CHANNEL_0, &adc_raw);
        printf("Raw %d = %dmV\n", adc_raw, calcMilliVolt(adc_raw));
        vTaskDelay(1000/portTICK_PERIOD_MS);
    }
}

Il ne me reste plus qu’à câbler un potentiomètre sur mon ESP32, compiler et flasher ce code pour avoir une sortie terminal comme dans l’image ci-dessous.

Nous avons donc vu comment faire fonctionner le CAN, il reste désormais à envoyer les données dans la mémoire EEPROM.

Transfert des mesures du CAN vers l’EEPROM

Pour éviter de surcharger mon EEPROM, je vais prendre une dizaine de mesures, stockées dans un buffer, que j’enverrai ensuite. Comme l’ADC me retourne une valeur sur 12bits, il me faudra caster ces données vers un buffer de uint8_t. Avant d’envoyer les données vers l’EEPROM, faisons un dry run mesure > cast > display, avec le code ci-dessous.

main.c

#include <stdio.h>
#include "stdint.h"
#include <string.h>
#include "driver/i2c_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "hal/adc_types.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_sleep.h"

#define CNFREGADDR 0b1011000
#define CNFREGWA0 0b10001000
#define CNFREGWA1 0b00000000
#define LOCK 0x99
#define NOLOCK 0x66
#define LENGTH 48

i2c_master_bus_config_t config = {
    .clk_source = I2C_CLK_SRC_DEFAULT,
    .i2c_port = I2C_NUM_0,
    .scl_io_num = 22,
    .sda_io_num = 21,
    .glitch_ignore_cnt = 7,
    .flags.enable_internal_pullup = true,
};

i2c_master_bus_handle_t hi2c0;

i2c_device_config_t devconfig = {
    .dev_addr_length = I2C_ADDR_BIT_LEN_7,
    .device_address = CNFREGADDR,
    .scl_speed_hz = 100000,
};
i2c_device_config_t eepromconfig = {
    .dev_addr_length = I2C_ADDR_BIT_LEN_7,
    .device_address = 0b1010000,
    .scl_speed_hz = 100000,
};
i2c_master_dev_handle_t confreg_handle;
i2c_master_dev_handle_t eeprom_handle;

static void disp_buf(uint8_t *buf, int len)
{
    int i;
    for (i = 0; i < len; i++) {
        printf("%02x (%b)", buf[i], buf[i]);
        if ((i + 1) % 16 == 0) {
            printf("\n");
        }
    }
    printf("\n");
}

int calcMilliVolt(int adcValue) {
    return((3300*adcValue)/4095);
}

int adc_raw;
uint16_t voltage[10];

uint8_t write_buffer[10*2];

void app_main(void)
{
    //esp_sleep_enable_timer_wakeup(2000); // Time in μs
    // Initialisation de l’ADC1
    adc_oneshot_unit_handle_t hadc1;
    adc_oneshot_unit_init_cfg_t adc_config = {
        .unit_id = ADC_UNIT_1,
    };
    adc_oneshot_new_unit(&adc_config, &hadc1);
    // Configuration d’un channel l’ADC1
    adc_oneshot_chan_cfg_t chan_config = {
        .atten = ADC_ATTEN_DB_12,
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    adc_oneshot_config_channel(hadc1, ADC_CHANNEL_0, &chan_config);
    for(int i=0; i<10; ++i) {
        adc_oneshot_read(hadc1, ADC_CHANNEL_0, &adc_raw);
        voltage[i] = calcMilliVolt(adc_raw);
        printf("Raw %d = %dmV\n", adc_raw, voltage[i]);
        vTaskDelay(1000/portTICK_PERIOD_MS);
    }

    uint8_t read_buffer[LENGTH];
    i2c_new_master_bus(&config, &hi2c0);
    i2c_master_bus_add_device(hi2c0, &devconfig, &confreg_handle);
    memcpy(write_buffer, &voltage, sizeof(voltage));
    printf("Here is what is contained in my buffer");
    disp_buf(write_buffer, 20);
}

J’obtiens la sortie suivante

Raw 0 = 0mV
Raw 0 = 0mV
Raw 0 = 0mV
Raw 1180 = 950mV
Raw 1983 = 1598mV
Raw 2661 = 2144mV
Raw 4095 = 3300mV
Raw 4095 = 3300mV
Raw 4095 = 3300mV
Raw 4095 = 3300mV
Here is what is contained in my buffer00 (b)00 (b)00 (b)00 (b)00 (b)00 (b)b6 (b)
03 (b)3e (b)06 (b)60 (b)08 (b)e4 (b)0c (b)e4 (b)0c (b)e4 (b)0c (b)e4 (b)0c (b)

Reconstituons les paires d’octets.

[1] 00 00 
[2] 00 00
[3] 00 00
[4] b6 03
[5] 3e 06
[6] 60 08
[7] e4 0c
[8] e4 0c
[9] e4 0c
[10] e4 0c

Si je regarde la 4ème paire, j’ai b603, soit 46 595 en base 10… Pas du tout le 950 que j’ai mesuré. Sauf si…

Sauf si la présentation des bytes est little-endian, c’est à dire avec l’octet de poids faible en premier. Si j’inverse les deux bytes (03b6), je retrouve bien mon 950 !

C’est un point à garder en tête pour la suite, notamment si (comme c’est le cas) je souhaite lire les données de la 24CSM01 avec une autre plateforme. En tous cas, si je le re-caste sur l’ESP32 avec memcpy(voltage, &write_buffer, 20);, je retrouve bien mon tableau de données de base.

Le code complet pour l’ESP32 pour réaliser les mesures à l’ADC, les stocker puis les récupérer, est donné ci-dessous.

main.c

#include <stdio.h>
#include "stdint.h"
#include <string.h>
#include "driver/i2c_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "hal/adc_types.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_sleep.h"

#define CNFREGADDR 0b1011000
#define CNFREGWA0 0b10001000
#define CNFREGWA1 0b00000000
#define LOCK 0x99
#define NOLOCK 0x66
#define LENGTH 48

i2c_master_bus_config_t config = {
    .clk_source = I2C_CLK_SRC_DEFAULT,
    .i2c_port = I2C_NUM_0,
    .scl_io_num = 22,
    .sda_io_num = 21,
    .glitch_ignore_cnt = 7,
    .flags.enable_internal_pullup = true,
};

i2c_master_bus_handle_t hi2c0;

i2c_device_config_t devconfig = {
    .dev_addr_length = I2C_ADDR_BIT_LEN_7,
    .device_address = CNFREGADDR,
    .scl_speed_hz = 100000,
};
i2c_device_config_t eepromconfig = {
    .dev_addr_length = I2C_ADDR_BIT_LEN_7,
    .device_address = 0b1010000,
    .scl_speed_hz = 100000,
};
i2c_master_dev_handle_t confreg_handle;
i2c_master_dev_handle_t eeprom_handle;

static void disp_buf(uint8_t *buf, int len)
{
    int i;
    for (i = 0; i < len; i++) {
        printf("%02x ", buf[i]);
        if ((i + 1) % len == 0) {
            printf("\n");
        }
    }
    printf("\n");
}

int calcMilliVolt(int adcValue) {
    return((3300*adcValue)/4095);
}

int adc_raw;
uint16_t voltage[10];

uint8_t write_buffer[22];

void app_main(void)
{
    // Initialisation de l’ADC1
    adc_oneshot_unit_handle_t hadc1;
    adc_oneshot_unit_init_cfg_t adc_config = {
        .unit_id = ADC_UNIT_1,
    };
    adc_oneshot_new_unit(&adc_config, &hadc1);
    // Configuration d’un channel l’ADC1
    adc_oneshot_chan_cfg_t chan_config = {
        .atten = ADC_ATTEN_DB_12,
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    adc_oneshot_config_channel(hadc1, ADC_CHANNEL_0, &chan_config);
    for(int i=0; i<10; ++i) {
        adc_oneshot_read(hadc1, ADC_CHANNEL_0, &adc_raw);
        voltage[i] = calcMilliVolt(adc_raw);
        printf("Raw %d = %dmV\n", adc_raw, voltage[i]);
        vTaskDelay(1000/portTICK_PERIOD_MS);
    }

    uint8_t read_buffer[LENGTH];
    i2c_new_master_bus(&config, &hi2c0);
    i2c_master_bus_add_device(hi2c0, &eepromconfig, &eeprom_handle);
    memcpy(write_buffer+2, &voltage, sizeof(voltage));
    write_buffer[0] = 0;
    write_buffer[1] = 0;

    printf("Here is what is contained in my buffer\n");
    disp_buf(write_buffer, 22);
    printf("The first 2 chars are Memory Location\n");

    printf("Now sendig 10 values into EEPROM\n");
    ESP_ERROR_CHECK(i2c_master_transmit(eeprom_handle, write_buffer, 22, -1));
    printf("Sent !\n");

    vTaskDelay(1000/portTICK_PERIOD_MS);

    printf("Now trying to get it back\n");
    uint8_t addr_buf[2] = {0, 0};
    uint8_t data_buf[20];
    i2c_master_transmit_receive(eeprom_handle, addr_buf, 2, data_buf, 20, -1);
    printf("This is the buffer:\n");
    disp_buf(data_buf, 20);
}

Et en fonctionnement, on parvient bien à retrouver en sortie notre tableau de données.

Reste maintenant à l’exploiter depuis une autre plateforme, par exemple avec un Arduino UNO R3. À la base, je voulais exploiter ma bibliothèque Mem24CSM01 pour re-lire les données, mais… Ça ne fonctionne pas ! Par contre, en communiquant directement avec les fonctions de Wire.h (voir exemple ci-dessous), j’arrive à récupérer les données. Il y a donc visiblement un problème dans mon implémentation de Mem24CMS01 que je vais devoir corriger. Je ferai le point dans un prochain post sur l’origine de ce bug & sur le code Arduino pour récupérer les données.

24CSM01_Read.ino

#include "Mem24CSM01.h"
#include <Wire.h>
#define MEM 20

Mem24CSM01 memory(false, false);

void setup() {
    Serial.begin(9600);

    Wire.begin();
    // Disabling Software WP (in case it was active)
    //memory.disableSoftwareWriteProtect();

    MEMORYRESULT opResult;
    uint8_t byteArrayRead[MEM] = {0};
    Wire.beginTransmission(0b1010000);
    Wire.write(0);
    Wire.write(0);
    Wire.endTransmission();
    delay(10);
    Wire.requestFrom(0b1010000, MEM);
    while(Wire.available()){
        Serial.print(Wire.read(), HEX);
        Serial.print(" ");
    }
}


void loop() {
    // Nothing here
}

Bibliographie