Si comme moi vous utilisez des capteurs à ultrasons HC-SR04, alors vous savez à quel point ils sont contraignants à utiliser, pour mesurer une distance. Car pour chaque mesure, il faut soi-même gérer l’impulsion à faire sur la ligne TRIG, puis mesurer le temps d’ÉCHO en retour. Sans parler du fait que ça mobilise pas mal de temps-processeur pour ce faire, à chaque fois.
Du coup m’est venu l’idée de faire une carte PCB intermédiaire, qui demanderait au HC SR04 de faire des mesures en permanence, et qui retournerait la dernière valeur mesurée à la demande, via une simple requête I2C. Ainsi, plus de surcharge processeur, car toute la logique de contrôle/mesure/calcul serait déporté sur celle-ci ! Alors voyons sans plus attendre cette carte, que j’ai nommé : Adaptateur I2C pour capteur HCSR04 🙂
Remarque : dans un article précédent, nous avons déjà vu qu’il existait une version « P » du module HC-SR04, qui offrait justement un accès I2C (et pas que) pour faire des mesures de distances par ultrasons. Mais nous avons également vu que cette version HC-SR04P fonctionnait très mal en i2c, et c’est justement pour cette raison que j’ai construis la présente carte !
Présentation de cet Adaptateur I2C pour module ultrasonique HC-SR04
Pour commencer, voici un aperçu du PCB de cette carte adaptatrice I2C/HC-SR04, une fois soudée (avec plusieurs annotations dessus, pour vous permettre de bien repérer quels sont les éléments importants ici) :

Sur cette image, on retrouve :
- Un connecteur permettant de recevoir un module ultrasonique HC-SR04 (modèle traditionnel, avec les lignes TRIG et ECHO donc)
- Un connecteur de programmation, qui permettra d’alimenter la carte et programmer le microcontrôleur ATtiny 412 présent sur cette carte, en UPDI (à l’aide d’un convertisseur USB/UPDI compatible 5V, tel que celui que je vous avais déjà présenté)
- Un connecteur de communication I2C, qui permettra d’alimenter la carte et recevoir les lignes SDA et SCL (bus i2c), afin de pouvoir communiquer chaque dernière mesure de distance effectuée lorsque souhaité
- Un microcontrôleur ATtiny412, véritable cœur de ce projet, faisant l’interface entre le bus I²C et le module HCSR04 qui sera branché sur cette carte
- Deux résistances pull-up, avec leurs jumpers associés, à souder ou non selon si on souhaite rendre ces résistances actives ou non, sur les lignes SCL et SDA
- Trois jumpers de sélection d’adresse I2C, permettant à cette carte (périphérique/esclave i2c) de choisir 1 des 8 adresses I²C possibles (2 à la puissance 3 jumpers donnant 8 combinaisons possibles !)
- Un réseau R/2R, constitué de 6 résistances, permettant de fournir une tension unique à partir de ces 8 adresses i2c possibles, selon l’état des jumpers de sélection d’adresse
ATTENTION : seul l’un des 2 connecteurs (programmation ou communication I2C) devra être branché sur cette carte, et en aucun cas les deux en même temps. Sans quoi vous risqueriez d’initier un conflit de tension potentiellement destructeur, à partir de là ! Donc attention à brancher soit l’un soit l’autre de ces deux connecteurs d’alim, mais jamais les deux simultanément !
Du reste, au niveau fonctionnel, l’utilisation de cette carte est vraiment simple ! Car il suffit d’interroger cette I²C, pour qu’elle retourne une valeur 16 bits (sur deux octets donc), donnant la dernière valeur mesurée en millimètres.
Par exemple :
- si l’adresse de cette carte I2C est 0x30
- et s’il y a un obstacle à 26 cm devant le module HC-SR04
- et si on demande à la carte de nous retourner une valeur
- alors la carte devrait théoriquement retourner la valeur 260, qui correspond à 260 mm, soit 26 cm
Difficile de faire plus simple, avouez !
Nota 1 : cette carte adaptatrice I2C va continuellement effectuer des mesures de distance, à partir du HC-SR04. Et lorsque vous solliciterez une mesure via le bus i2c, ce sera tout simplement la dernière valeur mesurée à l’instant qui vous sera retournée.
Nota 2 : dans l’exemple ci-dessus, j’ai choisi l’adresse i2c 0x30. Mais sachez que comme cette carte adaptatrice HC-SR04 dispose de 3 jumpers (bits) de sélection d’adresse, vous aurez donc le choix entre 8 valeurs possible. Et comme l’adresse de base enregistrée par défaut en dur est 0x30, vous aurez donc au final la possibilité de choisir par les 8 valeurs I²C suivantes : 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, ou 0x37.
Nota 3 : à la mise sous tension, il faudra attendre 1 à 2 secondes avant d’interroger cette carte, le temps pour elle de pouvoir correctement s’initialiser (paramétrage registres/interruptions, lecture adresse i2c esclave, et faire des premières lectures).
Schéma électronique
Maintenant que nous avons vu la présentation de cet adaptateur I2C pour module HCSR04, zoomons un peu, et voyons toute la partie électronique, à proprement parler !
Vue d’ensemble
Pour commencer, voici le schéma électronique de cet Adaptateur I²C pour capteur à ultrasons HC-SR04 (nota : si cette image ne vous paraît suffisamment nette, n’hésitez pas à faire un saut en fin d’article, où vous trouverez une version PDF de ce schéma, sur laquelle vous pourrez zoomer !) :

Comme d’habitude, l’ensemble a été divisé en plusieurs blocs, pour faciliter la lecture. On retrouve donc ici :
- un bloc « Bornier alim/pilotage I2C », qui permet à la fois d’alimenter ce projet en 5 volts et d’accueillir les lignes du bus i2c (SDA et SCL)
- un bloc « Pull-up I2C », qui permet de connecter ou non une résistance pull-up sur chaque ligne I²C (SDA et SCL, donc). Pour rappel, il faut à minima une résistance pull-up par ligne i2c, et veiller à ne pas trop les multiplier (sans quoi le courant sur le bus va monter proportionnellement, jusqu’à devenir trop important, voire destructeur). C’est pourquoi ces résistances sont activables ou désactivables ici, au besoin !
- un bloc « Sélecteur d’adresses I2C », composé de 3 doubles cavaliers (jumpers), permettant d’avoir 3 bits de configuration pour l’adressage i2c, donc 8 adresses I2C possibles (23_jumpers)
- un bloc « Convertisseur numérique/analogique », qui est ni plus ni moins qu’un réseau R/2R, permettant, à partir des 3 bits/cavaliers/jumpers précédents, de délivrer une tension unique correspondante (entre 0 et quasi 5V). Ceci permet donc de n’utiliser qu’1 seule entrée du microcontrôleur au lieu de 3, pour déterminer une adresse I2C à partir de ces 3 jumpers !
- un bloc « Microcontrôleur (ATtiny412) », qui, comme son nom l’indique, contient le cœur de ce projet, à savoir un µC ATtiny 412
- un bloc « Capteur HC-SR04 », qui est tout bonnement un bornier de raccordement permettant de recevoir n’importe quel module traditionnel HC-SR04 (avec lignes TRIG/ECHO), accompagné d’un condensateur de filtrage et un de découplage, afin de stabiliser son alimentation
- et un bloc « Bornier alim/prog », qui contient tout simplement le bornier de programmation de l’ATtiny (avec une ligne d’alim +5V, une ligne de prog UPDI, et la masse)
En sachant que vous pouvez faire fi des autres blocs, qui sont :
- le bloc « ERC Kicad », qui est juste là pour faire du contrôle de règles électriques sous Kicad
- et le bloc « Trous de montage », qui permet d’auto-importer les empreintes correspondantes, lors de l’ajout automatique des composants au niveau du PCB
Comme vous pouvez le constater, ce schéma est extrêmement simple, mis à part peut-être le réseau R/2R (le fameux « convertisseur numérique vers analogique »), pour ceux qui ne connaissent pas ! D’ailleurs, si tel est votre cas, où si vous souhaitez une piqûre de rappel, voici ce que c’est et comment il fonctionne !
Fonctionnement du réseau R/2R (convertisseur numérique → analogique)
Un réseau R/2R est une topologie de montage électronique en échelle, constitué de résistances ne pouvant prendre que deux valeurs : soit R, soit 2R (avec R une résistance de valeur quelconque, et 2R une résistance de valeur double par rapport à R). On le retrouve dans le schéma électronique précédent, avec R=10K, et donc 2R=20K. Vu autrement, sans les valeurs, ce réseau ressemble à cela :

À noter qu’un réseau R/2R a :
- autant d’entrées qu’on souhaite (3 sur ce schéma)
- et une seule sortie (toujours)
Et qu’au niveau des tensions :
- on envoie soit la masse, soit Vcc, au niveau des entrées (c’est pourquoi on parle souvent de « bits d’entrée », du fait que ces entrées ne peuvent prendre que l’état « 0 » ou « 1 », selon si on envoie la masse ou +Vcc)
- et on récupère un signal analogique en sortie (plus précisément une tension fixe, dont la valeur peut aller de 0 compris à quasiment Vcc, en fonction de l’état des entrées)
Comme vous vous en doutez, le fait d’avoir choisi des valeurs de résistances R et 2R (donc simple ou double) n’est pas un hasard ! En effet, cela permet d’obtenir des divisions de tension successives par deux. Ce qui signifie que la 1ère entrée aura comme « poids » une tension de Vcc/2, la 2ème entrée Vcc/4, la 3ème entrée Vcc/8, puis Vcc/16, puis Vcc/32, etc.
Par exemple, dans le cas d’un réseau R/2R à 3 entrées (donc à 3 bits, puisque les entrées ne peuvent recevoir que 0=masse ou 1=Vcc), la tension de sortie est égale à :
Ce qui donne le tableau suivant, en fonction de toutes les combinaisons possibles en entrée (23_entrées, donc 8 possibilités) :
| Entrée_2 | Entrée_1 | Entrée_0 | Vsortie |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 1/8 * Vcc |
| 0 | 1 | 0 | 2/8 * Vcc |
| 0 | 1 | 1 | 3/8 * Vcc |
| 1 | 0 | 0 | 4/8 * Vcc |
| 1 | 0 | 1 | 5/8 * Vcc |
| 1 | 1 | 0 | 6/8 * Vcc |
| 1 | 1 | 1 | 7/8 * Vcc |
Ainsi, vous voyez que la sortie d’un réseau R/2R à 3 bits a une tension pouvant aller de 0 à 7/8ème de Vcc inclus. Donc si VCC=5V, par exemple, alors la tension de sortie ira de 0 à 4,375 volts (4,375V = 7/8 de 5V), en fonction des entrées. Dit de manière encore plus explicite, la tension de sortie ne pourra prendre que les valeurs suivantes : 0 V, 0.625 V, 1.25 V, 1.875 V, 2.5 V, 3.125 V, 3.75 V, ou 4.375 V !
Nota 1 : vous remarquerez au passage qu’on atteint jamais la tension +Vcc, lorsqu’on met tous les bits à 1 (toutes les entrées à +Vcc, si vous préférez). Mais ce n’est pas grave, car on obtient bien l’effet escompté, à savoir : obtenir 8 tensions différentes à partir des 8 combinaisons possibles (8 = 2 à la puissance 3, puisque nous avons 3 entrée ici).
Nota 2 : toutes ces valeurs sont théoriques, gardez toujours cela à l’esprit. En effet, en pratique, tout est en fonction des valeurs réelles et comportement des résistances utilisées. Par exemple : si vous utilisez des résistances R de 10K, qui en réalité font 10.07K, 10.03K, et 9.98K, alors vous n’aurez pas exactement les tensions escomptées en sortie, car elles ne font pas toutes exactement 10K ohms. Mais peu importe ! Car les tensions réelles seront toujours « aux alentours » de la tension théorique attendue. Il suffit donc de prendre cela en compte, dans le code de programmation d’un µC lorsqu’on fait une mesure de tension (ADC), en recherchant une tension dans une zone autour d’une valeur attendue, et non chercher une valeur exacte (vous verrez d’ailleurs un peu plus bas, dans le code ATtiny, comment j’ai codé tout ça, si vous voulez un exemple pratique !).
Liste des composants
Pour info, pour ceux que ça intéresse, voici la liste des composants utilisés dans ce projet d’adaptateur i2c pour module HC SR04 :
En espérant n’avoir rien oublié, comme d’habitude 😉
Soudage des composants
À présent, passons à la partie soudage ! Mais avant cela, laissez-moi vous montrer le PCB de cette carte I2C pour HC-SR04, recto/verso, avant que les composants électroniques ne soient soudés dessus :

Comme vous pouvez le constater, les composants (CMS et traversants) sont tous placés du même côté, l’autre côté étant ni plus ni moins qu’un plan de masse.
Perso, pour aller vite, j’ai soudé composants CMS et traversants au fer à souder. Donc ne faites pas trop attention à mes soudures, si celles-ci sont un peu trop chargées ! Et comme toujours, j’ai soudé les composants les plus bas/petits (donc les CMS) en premier, et tous les composants restants (traversants) ensuite ! Voici ce que ça donne, en images :



À partir de là, il ne reste plus qu’à mettre les cavaliers (jumper) pour la sélection d’adresse I2C sur PCB, et ça en sera fini pour cette partie ! Voici donc le rendu final, avec tous les composants/accessoires en place :

Bon… ! Nous voilà au terme de cette partie matérielle, alors passons à la partie logicielle 🙂
Programme principal + programmation ATtiny (cœur de la carte)
Dans cette partie, nous allons voir le programme faisant tourner cet adaptateur I2C pour module HC-SR04, puis le téléversement de ce programme (upload) dans le microcontrôleur ATtiny412, véritable cœur de ce montage !
Programme « Adaptateur I2C pour HC-SR04 » (écrit sous Arduino IDE)
Pour commencer, voyons ce programme ! Et juste avant de vous le détailler, quelques mots à son sujet :
- le code utilise des manipulations de registres microcontrôleur, pour être efficient. Mais ne prenez pas peur, car tout ceci n’est pas si compliqué que cela, du moment où vous avez la documentation technique (datasheet) du fabricant, qui détaille tout cela ! Je vous mets un lien vers le datasheet de l’ATtiny412 ici, ainsi que tout en bas de cet article, dans la zone « liens et téléchargements »
- j’ai écrit tout le code sous Arduino IDE, bien que ce soit pour ensuite l’uploader dans un µC ATtiny, et non sur une carte arduino (pour que ce soit plus simple/accessible au plus grand nombre). Au passage, tout ceci est permis grâce au gestionnaire de cartes « megaTinyCore », qu’il faudra avoir installé dans son IDE Arduino. Si ce n’est pas le cas, voici la procédure à suivre, pour installer megatinycore dans Arduino IDE :
- aller dans le menu Outils > Type de carte > Gestionnaire de carte
- taper « megatinycore » dans le champ de recherche et valider
- puis sélectionner la ligne « megaTinyCore by Spence Konde » (attention à ne pas vous tromper ici, avec les autres packages avoisinant !)
- puis cliquer sur Installer
Cela étant dit/fait, voyons à présent le code ATtiny de cet adaptateur I2C pour HCSR04, sans plus attendre !
// ===================================================================================================
// ______ _ _///_ _ _ _
// / _ \ (_) | ___| | | | (_)
// | [_| |__ ___ ___ _ ___ _ __ | |__ | | ___ ___| |_ _ __ ___ _ __ _ ___ _ _ ___
// | ___/ _ \| __|| __| |/ _ \| '_ \_____| __|| |/ _ \/ _| _| '__/ \| '_ \| |/ \| | | |/ _ \
// | | | ( ) |__ ||__ | | ( ) | | | |____| |__ | | __/| (_| |_| | | (_) | | | | | (_) | |_| | __/
// \__| \__,_|___||___|_|\___/|_| [_| \____/|_|\___|\____\__\_| \___/|_| |_|_|\__ |\__,_|\___|
// | |
// \_|
// ===================================================================================================
//
// Nom Projet : Adaptateur I2C pour Capteur à ultrasons HC-SR04
//
// Desc Projet : Programme "i2c esclave", tournant sur un ATtiny412, permettant d'effectuer
// des mesures continuelles de distance (écho) via un HC-SR04, et permettant de
// retourner la dernière valeur lue, à la demande, via le bus I2C
//
// Fichier : prgAdaptateurI2CpourHCSR04.ino
// Créé le : 10.05.2026
// Auteur : Jérôme TOMSKI
// Site : https://passionelectronique.fr/
// GitHub : https://github.com/PassionElectronique/Programmes-Adaptateur-I2C-pour-capteur-HCSR04
// Licence : https://creativecommons.org/licenses/by-nc-nd/4.0/deed.fr (BY-NC-ND 4.0 CC)
//
// ==============================================================================================================
//
// Prérequis : Avoir installé "megatinycore" dans Arduino IDE
// Prog ATtiny : Impérativement régler 'Clock' sur '20 MHz internal', dans le menu 'Outils' d'Arduino IDE,
// avant d'uploader ce code dans l'ATtiny412 (explications du pourquoi avoir choisi 20 MHz
// pour le CPU ATtiny dans la fonction 'configureHorlogePeripheriquesInternesATtiny')
//
// ==============================================================================================================
// Constantes
#define ADRESSE_I2C_DE_BASE 0x30 // Adresse i2c de base, choisie arbitrairement ici, pour ce projet
#define BROCHE_DE_SORTIE_TRIG PIN_PA3 // Sortie PWM pour envoyer des impulsions de 10 µs toutes les 100 ms environ, sur la ligne TRIG du HC-SR04 (demandes de mesures automatiques/perpétuelles)
#define BROCHE_D_ENTREE_ECHO PIN_PA7 // Nota : n'importe quelle pin libre aurait pu convenir ici (grâce à EVSYS, qui peut relier n'importe pin au timer TCB0, qui nous intéresse ici)
#define BROCHE_D_ENTREE_ADC PIN_PA6 // Entrée ADC, permettant de définir l'adresse I2C souhaitée parmi 8 adresses possibles (celle de base, décrite un peu plus haut, et les 7 suivantes)
// Variables
uint8_t adresse_I2C_reelle; // Tient compte de ADRESSE_I2C_DE_BASE spécifiée ci-dessus (0x30), et BROCHE_D_ENTREE_ADDR (permet d'ajouter de 0 à 7, pour avoir le choix entre 8 adresses i2c différentes)
uint8_t index_buffer_verrouille = 0; // On utilise 2 buffers ici, car la valeur 16 bits (2 octets) peut être altérée en lecture au moment d'une interruption (ISR)
uint16_t buffer_0_distance_mesuree_en_mm = 0;
uint16_t buffer_1_distance_mesuree_en_mm = 0;
volatile uint8_t index_octet_a_envoyer = 0; // Permet d'envoyer un octet après l'autre (si égal 0, alors le LSB va être envoyé ensuite ; si égal 1, alors c'est au tour du MSB)
// Vérification de sécurité, afin de s'assurer que le timer TCD0 est bien choisi pour millis() etc
#ifndef MILLIS_USE_TIMERD0
#error "Ce programme nécessite que le timer TCD0 soit utilisé pour millis()/micros(). Configurez-le dans le menu 'Outils' > 'millis()/micros() Timer'."
#endif
// ========================
// Initialisation programme
// ========================
void setup() {
// Configurations préalables
configureHorlogePeripheriquesInternesATtiny();
determineAdresseI2Csouhaitee();
configureCommunicationsI2CenModeEsclave();
configureSytemeDeCaptureLigneECHO();
configurePwmPersistantSurLigneTRIG();
// Activation des interruptions, et passage à la boucle loop
sei();
}
// =================
// Boucle principale
// =================
void loop() {
// Rien ici, car une fois les configurations initiales passées, "tout" se passe au niveau des ISR (en cas de capture sur la ligne ECHO, ou requête I2C)
}
// ======================================================
// Fonction : configureHorlogePeripheriquesInternesATtiny
// ======================================================
void configureHorlogePeripheriquesInternesATtiny() {
// ------------------------------------------------------------------------------------------------------------------------------------------------------------------
// Explications techniques
// ------------------------------------------------------------------------------------------------------------------------------------------------------------------
// But de cette fonction :
// -> fixer la vitesse d'avancement des timers internes de l'ATtiny à 1/16ème de la fréquence principale (1/16ème de 20 MHz, soit 1,25 MHz donc).
// Ainsi, chaque timer sera incrémenté de 1 toutes les 0.8 µs (1 / 1,25 MHz). Et comme les timer A et B qui seront utilisés dans ce programme
// sont codés sur 16 bits (comptant de 0 à 65535, donc), la période totale pour ces timers sera de 0,052428 seconde (soit 52 ms environ)
// Pourquoi avoir choisi 20 MHz comme fréquence principale ATtiny, et une division de fréquence par 16 pour les périphériques internes (timers, etc) ?
// -> car ces 52 ms de période couvrent bien les 38 ms MAX du signal ECHO retourné par le capteur HC-SR04 (timeout correspondant à une distance de 6,5m environ)
// -> car ces 52 ms de période, si multipliés par 2, nous donnent 104 ms de période, ce qui nous permettra de faire une mesure HC-SR04 toutes les 100ms environ
// (en sachant qu'il faut respecter un temps d'attente d'environ 60ms entre deux lectures, ces 100ms sont tout bonnement parfaits !)
// ------------------------------------------------------------------------------------------------------------------------------------------------------------------
CPU_CCP = CCP_IOREG_gc; // Levée éphémère de la protection du registre protégé suivant (MCLKCTRLB)
CLKCTRL.MCLKCTRLB = CLKCTRL_PDIV_16X_gc | CLKCTRL_PEN_bm; // Fixation du prescaler d'horloge sur DIVISON par 16, et activation de ce prescaler (PEN)
}
// =======================================
// Fonction : determineAdresseI2Csouhaitee
// =======================================
void determineAdresseI2Csouhaitee() {
// ------------------------------------------------------------------------------------------------------------------------------------------------------------------
// Explications techniques
// ------------------------------------------------------------------------------------------------------------------------------------------------------------------
// But de cette fonction :
// -> déterminer l'adresse I2C "finale" de cet ATtiny, parmi 8 possibles (en partant d'une adresse de base donnée tout en haut)
// Difficulté :
// -> l'ATtiny412 n'a que 8 broches, dont 7 sont déjà réservées (VCC, GND, UPDI, SDA, SCL, entrée_ECHO, et sortie_TRIG) ; cela ne laisse donc qu'une SEULE pin disponible
// Solution :
// -> passer par une tension analogique, à 8 niveaux de tension différente grâce à un réseau R/2R et 3 bits de commande (3 cavaliers/jumpers inverseurs en fait, sur PCB)
// -> lire cette tension analogique via une entrées analogique sur l'ATtiny, et la convertir en valeur numérique (paliers allant de 0 à 7, pour avoir au final 8 adresses i2c possibles)
// Petite pause de stabilisation (tension d'alim, et partie ADC)
delay(100);
// Constantes locales
const uint8_t nombre_de_lectures_ADC = 10;
const float tension_d_alim_ATtiny = 5.0;
const uint8_t pause_entre_chaque_lecture_ADC_en_millisecondes = 20;
const uint8_t nombre_de_niveaux_de_tension_ADC = 8; // Nos 3 cavaliers sur PCB nous donnent 3 bits de commande, soit 2 puissance 3 possibilités (2^3 = 8)
const float plage_de_tension_d_un_palier_ADC = tension_d_alim_ATtiny / nombre_de_niveaux_de_tension_ADC; // Avec Valim=5V et NbNiveaux=8, alors on aura 0,625 V par palier
// Relevé de la tension ADC (plus précisément : réalisation de 10 lectures ADC successives, espacées de 10 ms chacune, et calcul d'une valeur moyenne, pour filtrer les éventuelles parasites)
float tension_moyenne_ADC = 0.0;
for (uint8_t i = 0; i < nombre_de_lectures_ADC ; i++) {
int valeur_brute_entree_ADC = analogRead(BROCHE_D_ENTREE_ADC); // Lecture ADC (sur 10 bits, soit une valeur entre 0 et 1023 compris, sur un ATtiny)
float tension_mesuree_sur_ADC = valeur_brute_entree_ADC * (tension_d_alim_ATtiny / 1023.0);
tension_moyenne_ADC += tension_mesuree_sur_ADC;
delay(pause_entre_chaque_lecture_ADC_en_millisecondes);
}
tension_moyenne_ADC /= nombre_de_lectures_ADC;
// Remarques :
// -> les 8 tensions (paliers) théoriques possibles seront les suivantes : 0, 0.625V, 1.25V, 1.875V, 2.5V, 3.125V, 3.75V, et 4.375V
// -> on va donc considérer 8 plages possibles à partir de ces tensions, en acceptant une tension comprise entre -1/2 pas < tension_mesuree < +1/2 pas (en sachant qu'un pas vaut 0,625V)
// ainsi, si la tension mesurée n'est pas pile-poil sur sa valeur théorique, on pourra la retrouver en laissant un peu de marge dessus/dessous
// Détermination du palier choisi (entre 0 et 7 compris, qui se rajouteront à l'adresse I2C de base)
uint8_t numero_de_palier = 0;
for (uint8_t i = 0; i < nombre_de_niveaux_de_tension_ADC ; i++) {
float tension_basse_du_palier_i = (i == 0) ? 0 : (i - 0.5) * plage_de_tension_d_un_palier_ADC; // Nota : exception pour le "niveau 0", sinon ça donnerait une tension négative
float tension_haute_du_palier_i = (i + 0.5) * plage_de_tension_d_un_palier_ADC;
if (tension_moyenne_ADC >= tension_basse_du_palier_i && tension_moyenne_ADC < tension_haute_du_palier_i) {
numero_de_palier = i;
break;
}
}
// Enregistrement de l'adresse I2C finale, dans variable globale
adresse_I2C_reelle = ADRESSE_I2C_DE_BASE + numero_de_palier; // Si 0x30 est l'adresse I2C de base, alors l'adresse I2C finale pourra être 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, ou 0x37
}
// ==================================================
// Fonction : configureCommunicationsI2CenModeEsclave
// ==================================================
void configureCommunicationsI2CenModeEsclave() {
// -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
// Explications techniques
// -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
// But de cette fonction :
// -> configurer l'ATtiny en mode "esclave I2C", avec l'adresse i2c qui va bien (fonction de l'adresse de base spécifiée en haut, et de la position des switchs sur PCB
// Remarques :
// -> sur un ATtiny412 (SOIC-8), on a pin_PA1=SDA et pin_PA2=SCL ; mais nul besoin de configurer ces pins PA1/PA2 en sortie manuellement, car le TWI s'en charge
// -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
// Adresse I2C "esclave" (pour rappel, les adresses I2C sont codées sur 7 bits, auxquelles on rajoute un bit pour dire lecture ou écriture ; et ces 7 bits doivent figurer
// en premier dans "l'octet d'adresse i2c", on doit décaler ces bits d'une position vers la gauche)
TWI0.SADDR = (adresse_I2C_reelle << 1);
// Configuration du registre SCTRLA
TWI0.SCTRLA = TWI_ENABLE_bm // Bit 0 : active l'I2C slave
| TWI_SMEN_bm // Bit 1 : active le "Smart Mode" (simplifie le code, au niveau de l'ISR)
| TWI_PIEN_bm // Bit 5 : active les interruptions sur "Stop", si la ligne suivante (adresse ou stop) est active
| TWI_APIEN_bm // Bit 6 : active les interruptions sur "Address ou Stop" (début ou fin de transmission i2c)
| TWI_DIEN_bm; // Bit 7 : active les interruptions sur "Data" ("milieu" des transmissions I2C)
}
// ============================================
// Fonction : configureSytemeDeCaptureLigneECHO
// ============================================
void configureSytemeDeCaptureLigneECHO() {
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
// Explications techniques
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
// But de cette fonction :
// -> Permettre de mesurer de manière automatique les durées d'impulsions sur la ligne ECHO, et de déclencher une interruption après chaque impulsion détectée
// Rappel :
// -> les timers de notre ATtiny sont de base rythmés à 1,25 MHz (F_MAIN=20MHz et prescaler DIV/16) ; soit 52ms de période (en sachant que chaque impulsion sur
// la ligne ECHO fera au maximum 38ms, classiquement sur les HC-SR04)
// Remarques :
// -> On va utiliser l'Event System ATtiny (EVSYS) pour connecter la pin PA2 (retour ECHO de notre capteur ultrason HC-SR04) à l'entrée de capture du TimerB ATtiny (TCB0)
// -> Puis on va configurer ce timer TCB0 en mode "input capture pulse-width measurement" pour qu'il :
// - mette à zéro son compteur CNT à chaque front montant détecté (mode "normal" du pulse-width de ce timer, en fait)
// - déclenche une interruption (ISR) à chaque front descendant détecté (la valeur CNT sera automatiquement copiée dans CCMP à ce moment là)
// -> Et le nombre de ticks comptés (CCMP) par ce compteur TCB0 nous donnera la durée d'impulsion ECHO (en admettant qu'il n'y ait pas le moindre débordement de comptage,
// et du fait qu'on connait la vitesse d'avance du compteur TCB0, qui est fonction de F_MAIN=20MHz et du prescaler principal réglé en division de fréquence par 16)
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
// Configuration de l'Event System
EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN7_gc; // Relie Pin_PA7 à Event_CH0 (nota : on aurait pu prendre CH1, CH2, ou CH3 … c'était pareil ! J'ai arbitrairemnt pris CH0 ici)
EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // Relie Event_CH0 à TCB0 (nota : ASYNCUSER0=TCB0, en fait)
// Configuration du timer TCB0
TCB0.CTRLB = TCB_CNTMODE_PW_gc; // Sélection du mode "Pulse Width measurement"
TCB0.EVCTRL = TCB_CAPTEI_bm; // Active le système de capture d'événements (provoque : reset CNT à chaque front montant, et ISR à chaque front descendant avec valeur CCMP dispo)
TCB0.INTCTRL = TCB_CAPT_bm; // Active les interruptions (ISR) sur capture effective
TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // Prescaler d'horloge TCB0 sur division par 1 (pas de ralentissement ici, donc) et activation du timer TCB0
}
// =============================================
// Fonction : configurePwmPersistantSurLigneTRIG
// =============================================
void configurePwmPersistantSurLigneTRIG() {
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
// Explications techniques
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
// But de cette fonction :
// -> générer un signal PWM persistant sur la ligne TRIG, d'une fréquence de 10 Hz pour avoir une période de 100ms (le capteur HC-SR04A devant éviter d'être
// piloté à moins de 60ms d'intervalle, pour éviter les échos parasites)
// Rappel :
// -> les timers de notre ATtiny sont de base rythmés à 1,25 MHz (F_MAIN=20MHz et prescaler DIV/16) ; cela correspond à 52 ms environ de période
// Remarques :
// -> ici, pour générer une impulsion de 10µs (requis sur la ligne TRIG) toutes les 100ms, on va encore diviser la fréquence d'horloge timer par 2,
// pour arriver à une fréquence de 5 Hz, soit une période de 104 ms (qu'on va limiter à 100 ms, en fixant le registre PER du timer)
// -> on utilisera le timer TCA0, en mode "single-slope PWM"
// -> du fait de la division de fréquence par deux supplémentaire, le timer TCA0 avancera à un rythme de 625 kHz (1.25MHz/2), soit toutes les 1,6 µs exactement
// -> on utilisera les registres PERBUF et CMP0BUF au lieu de PER et CMP0, car c'est une "bonne pratique" pour mettre à jour la durée de période/impulsion PWM
// de manière synchrone, pour éviter les indésirables/transitoires (bien que ce soit inutile ici, du fait qu'on configure le timer une fois pour toute au démarrage,
// sans mise à jour ultérieure des registres ; mais comme c'est une "bonne façon de faire", je vous la montre !)
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
// Code indispensable, pour prendre le contrôle de TCA0 "contre" megatinycore // (cf. explications ici : https://github.com/SpenceKonde/megaTinyCore/blob/master/megaavr/extras/TakingOverTCA0.md)
PORTMUX.CTRLC = PORTMUX_TCA00_DEFAULT_gc; // Désactive le déplacement de WO0 sur PA7 via PORTMUX (fait par megatinycore, de base), en ramenant WO0 sur PA3 (indispensable sur les ATtiny à 8 broches, ayant WO0 et WO3 sur la même pin)
takeOverTCA0(); // Désactive les fonctionnalités megatinycore utilisant le timer TCA0, et réinitialise ce timer (pour éviter le forçage en mode split 2x8 bits de megatinycore, etc.)
// Configuration de la broche PA3 en sortie (c'est sur cette pin que sortira le signal PWM à destination de la pin TRIG)
PORTA.DIRSET = PIN3_bm;
// Configuration du timer TCA0
TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm // Active la sortie de génération de signal WO0 (qui va ressortir sur la pin PA3 de l'ATtiny)
| TCA_SINGLE_WGMODE_SINGLESLOPE_gc; // Active le mode "Single-Slope PWM Generation" (dans ce cas, CMP définit la durée d'impulsion, et PER la durée totale période)
TCA0.SINGLE.PERBUF = 62499; // Permet d'avoir une période PWM égale à 100 ms <--- (62499+1)*1,6µs = 0,1 seconde (soit 100 ms)
TCA0.SINGLE.CMP0BUF = 7; // Permet d'avoir une durée d'impulsion PWM égale à 11.2 µs <--- 7*1,6µ = 11,2 µs (pour rappel : il faut au moins 10 µs pour le HC-SR04)
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV2_gc // Divise la fréquence d'horloge reçue encore par 2 (donc 20MHz / 16 / 2 = 625 kHz)
| TCA_SINGLE_ENABLE_bm; // Active le timer TCA0, maintenant que tout est bien configuré !
}
// ========================================================================
// Interruption TCB0 (survient à chaque front descendant sur ECHO/PA2, ici)
// ========================================================================
ISR(TCB0_INT_vect)
{
// Lecture de la valeur capturée par TCB0
uint16_t nbre_de_ticks_ecoules = TCB0.CCMP;
// Effacement du drapeau d'interruption (impératif, sur tinyAVR)
TCB0.INTFLAGS = TCB_CAPT_bm;
// Conversion du nombre de ticks (TCB0.CCMP) en millimètres (mm). Pour cela, nous avons comme données :
// - un tick sur TCB0 équivaut à 0,8 µs (16/20MHz)
// - la vitesse du son est de 343.2 m/s (soit 0.3432 mm/µs)
// On peut ainsi en déduire le temps d'aller/retour (soit 0.27456 mm/0.8µs), et le temps d'un aller ou retour seul (la distance réelle à mesurer, donc) = 0.27456/2=0.13728 mm/0.8µs
// Donc pour 1 tick de 0.8 µs, la distance sera ainsi de 0.13728 mm.
// Du coup, si on fait TCB0.CCMP*0.13728, on obtient la distance en mm, séparant le capteur à ultrason HC-SR04 de l'obstacle réfléchissant
// Remarque technique : l'ATtiny412 n'ayant pas d'unité de calcul flottant, on évite ici de passer par des nombres à virgule (float),
// histoire d'aller vite pour ne pas perdre de temps dans cet ISR (sinon on risquerait de potentiellement retarder une interruption I2C.
// On préfère donc faire ici une multiplication puis division entière sur 32 bits (d'où les nombres marqués 'UL' ci-dessous, signifiant "unsigned long")
if (index_buffer_verrouille == 0) { // Pour éviter les données corrompues
buffer_1_distance_mesuree_en_mm = (nbre_de_ticks_ecoules * 13728UL) / 100000UL; // Nota : 13728 / 100000 = 0.13728 (astuce pour aller vite, dans le traitement des "nombres à virgule")
} else {
buffer_0_distance_mesuree_en_mm = (nbre_de_ticks_ecoules * 13728UL) / 100000UL;
}
}
// ======================================================
// Interruption I2C (survient à chaque "appel du maître")
// ======================================================
ISR(TWI0_TWIS_vect)
{
// Remarque : les interruptions I2C sont vraiment multiples ici ! En effet, on en aura une :
// - lorsqu'une adresse est reçue (sous-entendu qui correspond à l'adresse "esclave i2c" de ce projet, ou à l'adresse générale 0x00)
// - lorsque des données sont reçues
// - lorsqu'une erreur de bus est survenue
// - si un arrêt (STOP) a été détecté
// - etc
uint8_t status = TWI0.SSTATUS; // Nota : TWI0.SSTATUS est le registre du contrôleur TWI (Two-Wire Interface / I²C)
if (status & TWI_APIF_bm) { // Cas "Address" ou "Stop"
if (status & TWI_AP_bm) { // Cas "Address"
if (status & TWI_DIR_bm) { // Cas "Address" + "Master Read"
index_buffer_verrouille ^= 1; // Demande de lecture par le maître => on bascule de buffer (ce qui permet de travailler ici sur des données qui ne vont pas bouger malgré les ISR de TCB0, le temps que la transmission I2C soit achevée)
TWI0.SCTRLB = TWI_SCMD_RESPONSE_gc; // Répond pour poursuivre la transmission
} else { // Cas "Address" + "Master Write" (cas qui ne doit jamais arriver ici, sauf en cas d'utilisation d'un 'scanner I2C', c'est pourquoi j'ai géré ce cas ici)
TWI0.SCTRLB = TWI_SCMD_COMPTRANS_gc; // Termine la transmission
}
} else { // Cas "Stop"
TWI0.SCTRLB = TWI_SCMD_COMPTRANS_gc; // Termine la transmission
index_octet_a_envoyer = 0; // Réinitialise l'index de pointage d'octet, pour le tour d'après !
}
} else if (status & TWI_DIF_bm) { // Cas "Data byte" (uniquement en "Read")
if (index_octet_a_envoyer == 0) { // Envoi du LSB (partie basse du mot de 16 bits)
if (index_buffer_verrouille == 0) TWI0.SDATA = (uint8_t)buffer_0_distance_mesuree_en_mm; // LSB du buffer #0
if (index_buffer_verrouille == 1) TWI0.SDATA = (uint8_t)buffer_1_distance_mesuree_en_mm; // LSB du buffer #1
index_octet_a_envoyer = 1; // Passage de l'index à 0->1, pour que la fois d'après, ce soit le MSB qui soit envoyé
}
else if (index_octet_a_envoyer == 1) { // Envoi du MSB (partie haute du mot de 16 bits)
if (index_buffer_verrouille == 0) TWI0.SDATA = (uint8_t)(buffer_0_distance_mesuree_en_mm >> 8); // MSB du buffer #0
if (index_buffer_verrouille == 1) TWI0.SDATA = (uint8_t)(buffer_1_distance_mesuree_en_mm >> 8); // MSB du buffer #1
index_octet_a_envoyer = 2; // Valeur quelconque, différente de 0 ou 1, pour filtrer le "NACK du maître", qui déclenche une interruption également ici
} else {
TWI0.SDATA = 0; // Remarque : ce SDATA à 0 ne sera pas envoyé. Il est juste là pour ne pas bloquer la transmission avant de l'interrompre au niveau du cas "Stop" suivant
} // (puisque nos deux octets ont déjà été envoyés, on devrait juste avoir un passage par là, lors du NACK du maître)
}
}Comme vous avez pu le voir, je vous ai mis énormément de commentaires dans ce programme ! Ainsi, il vous sera plus facile de comprendre à quoi sert chaque ligne, ou presque ! Et bien sûr, libre à vous d’effacer tous ces commentaires pour alléger ce programme, et y voir plus clair 🙂
Au niveau du fonctionnement de ce programme, la fonction setup résume bien sur quoi repose cet adaptateur I2C pour HC-SR04 :
- on configure le diviseur d’horloge principal de l’ATtiny (qui tournera sur la base d’une fréquence CPU à « 20MHz interne », réduite ensuite ici pour coller à nos besoins spécifiques)
- on détermine l’adresse I2C choisie sur le PCB, à partir de la tension analogique retournée par le réseau R/2R (on fait même 10 lectures d’affilé pour avoir une tension moyenne bien stable, que l’on va ensuite comparer à nos seuils/zones de niveaux)
- on configure l’ATtiny pour qu’il fonctionne en « esclave I2C », c’est à dire pour qu’il devienne un véritable périphérique I²C, capable de répondre aux requêtes d’un « maître I2C »
- on configure un système de capture d’impulsion sur une broche spécifique de l’ATtiny, afin de pouvoir mesurer les durées d’impulsions de la ligne ECHO, retournée par le capteur à ultrasons HC-SR04
- on configure un signal PWM sur une autre broche spécifique de l’ATtiny, afin de générer des impulsions spécifiques, calibrées, et perpétuelles, à destination de la ligne TRIG de notre HC-SR04 (remarque : plus précisément, on générera ici une impulsion d’au moins 10µs toutes les 100ms, très exactement 11,2 µs toutes les 100 ms, comme le requiert l’émetteur/récepteur à ultrasons HC SR04)
- puis on autorise (active) les interruptions sur l’ATtiny, pour donner vie à l’I2C et au système de capture d’échos (issus du HCSR04)
Ainsi divisé ainsi en « petits blocs de code », vous verrez que tout n’est pas si sorcier que ça (même si l’ensemble peut paraître complexe au début !).
Upload de ce programme dans l’ATtiny
Maintenant que nous avons vu la partie code, il va falloir téléverser tout ce contenu dans le microcontrôleur ATtiny en lui-même ! Pour ce faire, il va falloir :
- paramétrer l’IDE Arduino, pour configurer l’ATtiny à programmer, en même temps que l’envoi du programme
- brancher le PCB de ce projet sur un convertisseur USB/UPDI afin de pouvoir programmer l’ATtiny depuis un ordi
Alors commençons par la configuration de l’ATtiny dans l’IDE Arduino, après ouverture du programme à uploader. Pour faire cette configuration, il suffit en fait d’aller dans le menu Outils de l’IDE, et configurer les choses ainsi :

Si l’image n’est pas très nette, voici les options que j’ai sélectionné :
- Type de carte : ATtiny 412/402/212/202
- Chip : ATtiny412
- Clock : 20 MHz internal (à respecter impérativement, car tous les calculs/configs de registres microcontrôleur reposent sur cette hypothèse de départ)
- millis()/micros() Timer : TCD0 (car on va respectivement utiliser les timers TCA0 pour la génération de signal PWM, et TCB0 pour la mesure d’impulsion ECHO)
- Wire Library Mode : Master or Slave (dans notre cas, il s’agira d’un « esclave i2c » uniquement)
- Port : mettre le port de votre programmateur d’ATtiny (convertisseur USB/UPDI), une fois qu’il sera branché
- Programmateur : Serial UPDI – SLOW, pour éviter les erreurs d’upload (comme la programmation est « très » rapide, ce n’est pas gênant d’utiliser le mode « SLOW » ici !)
Une fois tout cela paramétré, il ne reste plus qu’à brancher votre convertisseur USB/UPDI entre le port USB de votre ordi et le PCB du présent projet (broches +5V/UPDI/GND), et à uploader le programme. Voici comment ça se présente de mon côté, en image (avec les 3 fils de connexion carte <-> programmateur UPDI, et le câble USB vers l’ordi à droite) :

Au passage, lors de l’upload, vous verrez ceci tout en haut du terminal arduino :
Le croquis utilise 2060 octets (50%) de l'espace de stockage de programmes. Le maximum est de 4096 octets.
Les variables globales utilisent 15 octets (5%) de mémoire dynamique, ce qui laisse 241 octets pour les variables locales. Le maximum est de 256 octets.
Cela veut dire que l’ATtiny412 correspond parfaitement à nos besoins (50% de la mémoire prog et 5% de la RAM seront utilisés).
Du reste, toujours au niveau du terminal arduino après upload, vous devriez retrouver, quelque part dans la console, le texte suivant :
pymcuprog.programmer - INFO - Verifying...
Verify successful. Data in flash matches data in specified hex-fileCela veut dire que le programme ATtiny a bien été uploadé sur la carte de ce projet ! Et cela étant fait, nous pouvons à présent passer à la partie « tests » 🙂
Programmes de tests arduino
Maintenant que nous avons notre carte ATtiny (adaptateur I2C pour HC SR04) programmée, il va falloir la tester. Et comme cette carte est ni plus ni moins qu’un « esclave i2c », alors il suffira de prendre une carte Arduino et de lui envoyer des commandes I2C, pour voir ce qu’elle retourne !
Alors commençons par câbler cela, à savoir : la carte de ce projet (adaptateur I2C pour HCSR04) et une carte Arduino Uno (par exemple) :
| Borne sur PCB de ce projet | Borne sur carte Arduino |
|---|---|
| +5V | 5V |
| SCL | A5 (scl) |
| SDA | A4 (sda) |
| GND | GND |
En image, cela donne ceci :

Ainsi câblé, nous allons à présent pouvoir faire 2 tests successifs :
- vérifier si la carte de ce projet répond bien sur le bus I²C (et si tel est le cas, à quelle adresse)
- envoyer des demandes I2C toutes les secondes, et afficher ce qui est retourné par cette carte
Alors allons y, un test après l’autre !
Scanner I2C (pour vérifier si le périphérique i2c répond, et à quelle adresse sur le bus)
Le plus simple, pour vérifier si un esclave i2c est bien connecté au bus (et à quelle adresse), est d’utiliser ce qu’on appelle un « Scanner I2C ». En fait, il s’agit ni plus ni moins que d’un programme qui va interroger toutes les adresses I²C possibles (hormis les adresses 0 et 127 qui sont réservées), pour voir qui répond, et à quelle adresse.
Voici donc un exemple de programme « scanner i2c », fort utile au demeurant !
// ===================================================================================================
// ______ _ _///_ _ _ _
// / _ \ (_) | ___| | | | (_)
// | [_| |__ ___ ___ _ ___ _ __ | |__ | | ___ ___| |_ _ __ ___ _ __ _ ___ _ _ ___
// | ___/ _ \| __|| __| |/ _ \| '_ \_____| __|| |/ _ \/ _| _| '__/ \| '_ \| |/ \| | | |/ _ \
// | | | ( ) |__ ||__ | | ( ) | | | |____| |__ | | __/| (_| |_| | | (_) | | | | | (_) | |_| | __/
// \__| \__,_|___||___|_|\___/|_| [_| \____/|_|\___|\____\__\_| \___/|_| |_|_|\__ |\__,_|\___|
// | |
// \_|
// ===================================================================================================
//
// Nom Projet : Scanner I2C
//
// Desc Projet : Permet de scanner toutes les adresses I2C (de 1 à 126, du fait que 0 et 127 sont réservées),
// à partir d'un Arduino Uno ou équivalent
//
// Fichier : prgScannerI2C.ino
// Créé le : 05.08.2026
// Auteur : Jérôme TOMSKI
// Site : https://passionelectronique.fr/
// GitHub : https://github.com/PassionElectronique/Programmes-Adaptateur-I2C-pour-capteur-HCSR04
// Licence : https://creativecommons.org/licenses/by-nc-nd/4.0/deed.fr (BY-NC-ND 4.0 CC)
//
// =========================================================================================================
// Bibliothèque à inclure
#include <Wire.h>
// ========================
// Initialisation programme
// ========================
void setup() {
// Initialisation des communications séries
Serial.begin(9600);
Wire.begin();
// Affichage de démarrage, à destination du moniteur série d'Arduino IDE
Serial.println("===========");
Serial.println("Scanner I2C");
Serial.println("===========");
Serial.println();
Serial.println("Démarrage du scan...");
// Initialisation compteur de périphériques I2C trouvés
byte nombreDePeripheriquesTrouves = 0;
// Scanne les adresses I2C n°0 à 127, ou plutôt 1 à 126, du fait que les adresses 0 et 127 sont réservées
for (byte adresse_I2C = 1; adresse_I2C <= 126; adresse_I2C++) {
// Initialise la communication i2c
Wire.beginTransmission(adresse_I2C);
byte retour_I2C = Wire.endTransmission();
// Si "endTransmission" retourne la valeur 0, alors on signale le périphérique détecté à l'adresse en cours
if (retour_I2C == 0) {
Serial.print("Périphérique détecté à l'adresse 0x");
if (adresse_I2C < 16) Serial.print("0");
Serial.print(adresse_I2C, HEX);
Serial.print(" (decimal : ");
Serial.print(adresse_I2C);
Serial.println(")");
nombreDePeripheriquesTrouves++;
}
// Si "endTransmission" retourne une valeur différente de 0, alors on n'en tient pas compte (erreur ou périphérique absent, dans ce cas)
else {
// Ne rien faire
}
}
// Affichage du nombre de périphériques trouvés, sur le moniteur série
Serial.println("Scan terminé !");
Serial.println("");
Serial.print("Bilan = ");
if (nombreDePeripheriquesTrouves == 0) Serial.println("aucun périphérique I2C détecté");
else if (nombreDePeripheriquesTrouves == 1) Serial.println("1 seul périphérique détecté");
else {
Serial.print(nombreDePeripheriquesTrouves);
Serial.println(" périphériques détectés");
}
// Fin
Serial.println("");
Serial.println("(appuyez sur la touche RESET de votre arduino, pour relancer le scan, si besoin !)");
}
// =================
// Boucle principale
// =================
void loop() {
// Vide, car tout se passe dans la fonction setup()
}Comment on s’en sert ? En fait, c’est super simple ! Car il suffit d’uploader ce programme dans la carte Arduino Uno qu’on a câblé précédemment, puis d’ouvrir le Moniteur Série de l’IDE Arduino pour voir le résultat du scan. Tout simplement 🙂
Si tout se passe bien, vous devriez obtenir quelque chose comme ceci (ici, ma carte projet était configurée sur l’adresse 0x30, avec les jumpers sur A2/A1/A0 en position basse, donc) :

Vous pouvez d’ailleurs modifier les jumpers de la carte adaptatrice I2C pour HC SR04 pour sélectionner une autre adresse, mais il faudra couper l’alimentation de cette carte et la remettre ensuite, pour que le changement soit pris en compte (au démarrage du µC, en fait). Par exemple, si je mets A2 en position haute (A2=1), A1 en position haute (A1=1), et A0 en position basse (A0=0), alors c’est l’adresse 0x36 qui sera sélectionnée (comme décrit au dos du PCB du présent projet). Et on peut vérifier la nouvelle adresse i2c, en relançant le scanner I2C, comme visible ci-après :

Sinon, si aucun périphérique I2C n’est détecté (en cas de problème), alors un message d’erreur s’affichera à l’écran :

Maintenant que nous avons vu que la carte répond bien sur le bus I2C, à l’adresse sélectionnée, nous pouvons donc passer aux « vrais essais », à proprement parler !
Test de mesures de distances avec cet adaptateur I2C pour module HC-SR04
Arrive le moment tant attendu de l’essai final, pour voir si cette carte I2C/HC-SR04 fait bien son boulot !
Pour ce faire, voici le code qu’il faudra uploader dans l’Arduino Uno, pour ce faire :
// ===================================================================================================
// ______ _ _///_ _ _ _
// / _ \ (_) | ___| | | | (_)
// | [_| |__ ___ ___ _ ___ _ __ | |__ | | ___ ___| |_ _ __ ___ _ __ _ ___ _ _ ___
// | ___/ _ \| __|| __| |/ _ \| '_ \_____| __|| |/ _ \/ _| _| '__/ \| '_ \| |/ \| | | |/ _ \
// | | | ( ) |__ ||__ | | ( ) | | | |____| |__ | | __/| (_| |_| | | (_) | | | | | (_) | |_| | __/
// \__| \__,_|___||___|_|\___/|_| [_| \____/|_|\___|\____\__\_| \___/|_| |_|_|\__ |\__,_|\___|
// | |
// \_|
// ===================================================================================================
//
// Nom Projet : Adaptateur I2C pour Capteur à ultrasons HC-SR04
//
// Desc Projet : Programme de test pour l'adpatateur I2C pour module HC-SR04,
// fonctionnant sur Arduino Uno
//
// Fichier : prgTestAdaptateurI2CpourHCSR04.ino
// Créé le : 05.08.2026
// Auteur : Jérôme TOMSKI
// Site : https://passionelectronique.fr/
// GitHub : https://github.com/PassionElectronique/Programmes-Adaptateur-I2C-pour-capteur-HCSR04
// Licence : https://creativecommons.org/licenses/by-nc-nd/4.0/deed.fr (BY-NC-ND 4.0 CC)
//
// =========================================================================================================
// Bibliothèque à inclure
#include <Wire.h>
// Constantes
#define ADRESSE_I2C_ADAPTATEUR_HCSR04 0x30
// ========================
// Initialisation programme
// ========================
void setup() {
// Initialisation des communications séries
Serial.begin(9600);
Wire.begin();
// Affichage de démarrage, à destination du moniteur série d'Arduino IDE
Serial.println("=================================================");
Serial.println("Programme de test /// adaptateur I2C pour HC-SR04");
Serial.println("=================================================");
Serial.println();
// Petite pause, pour laisser le temps à l'adaptateur I2C pour HCSR04 de démarrer/se stabiliser
delay(1000);
}
// =================
// Boucle principale
// =================
void loop() {
// Interrogation de l'adaptateur I2C (demande 2 octets en retour)
Wire.requestFrom(ADRESSE_I2C_ADAPTATEUR_HCSR04, 2);
if (Wire.available() >= 2) {
// Si deux octets reçus, on les lit
uint8_t premier_octet = Wire.read();
uint8_t deuxieme_octet = Wire.read();
// Puis on en déduit la distance
uint16_t distance_mm = (uint16_t)deuxieme_octet << 8 | premier_octet;
// Et affichage (avec octets bruts en hexa, pour diagnostic temporaire)
Serial.print("Octets recus : 0x");
if (premier_octet < 0x10) Serial.print("0");
Serial.print(premier_octet, HEX);
Serial.print(" 0x");
if (deuxieme_octet < 0x10) Serial.print("0");
Serial.print(deuxieme_octet, HEX);
Serial.print(" => Distance : ");
Serial.print(distance_mm / 10.0, 1); // Division par 10 pour passer en 'centimètres', puis passage en valeur 'float' au passage, et affichage avec 1 chiffre après la virgule
Serial.println(" cm");
} else {
// Si l'adaptateur I2C n'a pas répondu, alors on affiche le fait qu'il n'y a pas de réponse retournée sur le bus I2C
Serial.println("[ERREUR] pas de réponse reçue...");
}
// Attend 1 seconde, et reboucle
delay(1000);
}Comme vous le voyez, ce programme de test I²C/HCSR04 est assez court, et se résume simplement en :
- demander la dernière mesure effectuée par la carte adaptatrice I2C (sur 2 octets, car la valeur mesurée est exprimée sur 16 bits) ; à noter que cette valeur est ni plus ni moins que la distance, en millimètres, séparant l’émetteur/récepteur HCSR04 d’un éventuel obstacle détectable devant lui
- afficher ces octets sur le moniteur série, et la transcription de ce nombre 16 bits en centimètres (pour que ce soit plus parlant !)
- et répéter cela de façon perpétuelle, à raison d’une demande de retour I2C par seconde !
Une fois uploadé, pour voir ce code en action, il suffit alors d’ouvrir le Moniteur Série de l’IDE Arduino, et mettre un obstacle en face du détecteur HC SR04 pour voir une distance s’afficher à l’écran toutes les secondes ! Pour ma part, j’ai mis une feuille de papier à environ 35 cm de distance, et puis l’ai rapprochée jusqu’à environ 2 cm du capteur HC-SR04. Et voici ce que j’ai pu relever sur mon moniteur série, à cette occasion :

Sympa, non ? On pourrait d’ailleurs masquer les « octets reçus » (dont l’affichage m’a servi au débogage) et n’afficher que la valeur en centimètres. Comme ça tout aurait été encore plus clair 🙂
Améliorations possibles
Juste une idée qui m’est venu à la fin : on aurait pu intégrer un petit régulateur de tension 3,3V sur cette carte, afin de fournir cette tension aux 2 résistances pull-up I2C. Ainsi, cette carte I2C pour HC-SR04 aurait été compatible avec les microcontrôleurs 5V ET 3,3V !
Bien sûr, cela ne change rien au fait qu’il faudrait dans tous les cas fournir une alimentation 5V à cette carte, et non 3,3 volts, car :
- le module HC-SR04 fonctionne en +5V « exclusivement » (mais on aurait pu opter pour un modèle HC-SR04P, fonctionnant aussi bien en 3,3 que 5 volts)
- l’ATtiny a besoin d’une alimentation de +5V pour tourner à une fréquence de 20 MHz (mais on aurait également pu re-paramétrer tous les registres, afin de tourner avec une cadence processeur plus faible, permise en 3,3 volts !)
Et pourquoi pas faire un upgrade de ce PCB un jour, notamment si y’a d’autres petites améliorations à apporter à ce montage 😉
Liens et téléchargements
Pour ceux que ça intéresse, vous trouverez ici :
- le schéma électronique de cet adaptateur I2C pour capteur HC-SR04, en version PDF
- le datasheet du microcontrôleur ATtiny412, en version PDF
- le programme ATtiny pour la carte I2C → module HC SR04, sur GitHub
- le programme Arduino Uno de scan de toutes les adresses I2C, sur GitHub
- le programme Arduino Uno pour les tests de mesures de distances en I2C, de l’émetteur/récepteur HCSR04, sur GitHub
- et le lien vers le circuit imprimé de ce projet (pour ceux qui souhaiteraient commander le PCB de ce projet, moyennant « quelques euros », via le fabricant PCBWay ; pour plus d’infos, voir la démarche/explications ci-dessous)
(*) COMMENT FAIRE FABRIQUER LE PCB DE CE PROJET ?
→ Cliquer sur le lien juste ci-dessus (pour ouvrir le projet partagé sur PCBWay)
→ Cliquer sur « Add to cart » (pour l’ajouter à votre panier)
→ Attendre quelques minutes (24/48h maximum) pour que les fichiers de ce projet soient vérifiés (surveiller l’état d’avancement des éléments dans le panier ; ils resteront grisés tant que ça n’a pas été validé, et colorés une fois fait)
→ Finaliser la commande de circuit imprimé (en ajoutant un stencil, au besoin !)
Adaptateur I2C pour HC SR04 : conclusion !
Voilà ! Nous voici au terme de cet article sur cet Adaptateur I2C pour module HC-SR04 ! J’espère qu’il vous aura plu, et saura vous inspirer pour vos prochains projets !
Par ailleurs, c’est un projet qui est très intéressant sur le plan matériel (réseau R/2R) et très riche sur le plan logiciel, car il permet de vous montrer comment, sur un microcontrôleur ATtiny :
- générer un signal PWM (via le timer TCA0)
- mesurer une durée d’impulsion (avec le timer TCB0)
- et configurer un mode « esclave I2C », pour transformer la carte en véritable périphérique I²C !
Sur ce je vous laisse, et vous dis à bientôt, pour un prochain tuto 😉
Jérôme.
À découvrir aussi : un convertisseur I2C/PWM pour servomoteur
(*) Mis à jour le 13/08/2026

Salut,
Très gros travail, intéressant et formateur, bravo et merci
Merci ! Et de rien 🙂