CH32V307 : logique applicative

par Vincent DEFERT, dernière mise à jour le 2025-06-09

Ce que vous allez faire

Nous avons déjà développé 2 modules importants, l'affichage et le clavier. La suite logique est d'implémenter la logique applicative. Or, celle-ci aura besoin de 2 autres modules que nous n'avons pas encore et que nous devrions alors développer et mettre au point en plus de la logique applicative. C'est l'occasion pour nous d'aborder le thème important des "mocks".

Les mocks

Un "mock" est un morceau de code qui simule sommairement une fonctionnalité, souvent en ne faisant rien ou en renvoyant toujours la même valeur. Ça peut sembler bête de prime abord, mais c'est très utile au quotidien, notamment dans les situations suivantes :

  • Quand un projet est développé en équipe, chacun utilise des mocks à la place des modules développés par ses collègues afin de pouvoir avancer sur son propre module. Les mocks sont ensuite remplacés par les modules terminés au fur et à mesure de leur disponibilité.

    C'est précisément dans ce cas que nous allons nous placer, car rien n'empêche d'utiliser la même approche quand on travaille seul.

  • Quand on développe un test unitaire, un sujet important sur lequel nous reviendrons ultérieurement, le module testé peut avoir besoin de données provenant de l'extérieur. Utiliser un mock permet d'avoir l'assurance que ces données seront toujours les mêmes, ce qui permet de contrôler facilement le résultat dans le code de test.

Pour en illustrer le principe, je vais vous proposer ici les 2 mocks dont nous avons besoin. Si on se réfère aux spécifications de notre réveil, le mock du module chargé d'émettre des bips doit assurer les fonctions suivantes :

  • Émettre un bip court pour signaler une erreur de saisie
  • Émettre un bip continu pour réveiller le dormeur
  • Arrêter le bip de réveil
  • Déterminer si le bip de réveil est en cours d'émission

Nous en déduisons facilement le fichier beeper.h :

#ifndef _BEEPER_H
#define _BEEPER_H

/**
 * Initialises the beeper hardware.
 */
void beepInitialise();

/**
 * Starts a continuous alarm beep.
 */
void beepStartAlarm();

/**
 * Stops the alarm beep.
 */
void beepStopAlarm();

/**
 * Checks whether the alarm is ringing.
 *
 * @return true if the alarm beep is active.
 */
bool beepIsAlarmRinging();

/**
 * Emits a single error beep.
 */
void beepEmitError();

#endif // _BEEPER_H

Pour implémenter le mock, il suffirait de ne rien faire dans chaque fonction, mais on peut en profiter pour ajouter des printf() pour voir si on passe bien dans chaque fonction au moment où on est censé le faire. Ça nous donne le fichier beeper.c suivant :

#include "project-defs.h"
#include "beeper.h"

static struct {
	bool alarmRings;
} _state = {
	.alarmRings = false,
};

void beepInitialise() {
}

void beepStartAlarm() {
	printf("Alarm beep started\r\n");
	_state.alarmRings = true;
}

void beepStopAlarm() {
	printf("Alarm beep stopped\r\n");
	_state.alarmRings = false;
}

bool beepIsAlarmRinging() {
	return _state.alarmRings;
}

void beepEmitError() {
	printf("Error beep emitted\r\n");
}

j'ai l'habitude de créer dans chaque module une structure contenant ses données d'état, que j'appelle _state. Des variables indépendantes auraient aussi bien fait l'affaire, sauf que cette convention permet au premier coup d'œil de repérer qu'on manipule une donnée d'état du module et non un paramètre de la fonction ou une de ses variables locales. L'intelligibilité du code s'en trouve grandement améliorée, ce qui facilite sensiblement sa maintenance.

La démarche est la même pour le mock de l'horloge temps réel, sauf que nous nous apercevons rapidement que nous allons manipuler des objets "date et heure" et que définir un type pour les représenter améliorera grandement la clarté du code. Je vous propose le fichier datetime.h suivant :

#ifndef _DATETIME_H
#define _DATETIME_H

typedef struct {
	uint16_t year;
	uint8_t month;
	uint8_t day;
	uint8_t hour;
	uint8_t minute;
	uint8_t second;
} DateTime;

#endif // _DATETIME_H

Vous vous apercevrez en implémentant la logique applicative que vous aurez besoin de fonctions pour vérifier les dates et les heures. Vous pourrez donc les ajouter à datetime.h et les implémenter dans datetime.c. Nous pouvons maintenant revenir à l'écriture de l'entête du module de l'horloge temps réel, rtc.h :

#ifndef _RTC_H
#define _RTC_H

#include "datetime.h"

/**
 * Initialises the RTC peripheral.
 */
void rtcInitialise();

/**
 * Sets the RTC clock time.
 *
 * @param dt
 */
void rtcSetClockTime(const DateTime *dt);

/**
 * Gets the RTC clock time.
 *
 * @param dt the destination DateTime object's address. Must not be NULL.
 * @return dt, for convenience.
 */
DateTime *rtcGetClockTime(DateTime *dt);

/**
 * Sets the RTC alarm time.
 *
 * @param dt
 */
void rtcSetAlarmTime(const DateTime *dt);

/**
 * Gets the RTC alarm time.
 *
 * @param dt the destination DateTime object's address. Must not be NULL.
 * @return dt, for convenience.
 */
DateTime *rtcGetAlarmTime(DateTime *dt);

/**
 * Enables/disables the RTC alarm.
 *
 * @param enabled new alarm status.
 */
void rtcEnableAlarm(bool enabled);

/**
 * Checks whether the RTC alarm is enabled.
 *
 * @return the current RTC alarm status.
 */
bool rtcIsAlarmEnabled();

/**
 * Checks whether the RTC alarm was triggered.
 * Automatically clears the flag when set, so that it is ready for the next alarm event.
 *
 * @return true if the alarm was triggered.
 */
bool rtcIsAlarmRaised();

#endif // _RTC_H

Pour finir, je vous propose l'implémentation suivante pour le mock de l'horloge temps réel, rtc.c :

#include "project-defs.h"
#include "rtc.h"
#include <string.h>

static struct {
	DateTime clockTime;
	DateTime alarmTime;
	bool alarmEnabled;
	bool alarmRaised;
} _state = {
	.clockTime = {
		.year = 2025,
		.month = 1,
		.day = 1,
		.hour = 01,
		.minute = 23,
		.second = 45,
	},
	.alarmTime = {
		.year = 2025,
		.month = 1,
		.day = 1,
		.hour = 12,
		.minute = 34,
		.second = 56,
	},
	.alarmEnabled = false,
	.alarmRaised = false,
};

void rtcInitialise() {
}

void rtcSetClockTime(const DateTime *dt) {
	memcpy(&_state.clockTime, dt, sizeof(_state.clockTime));
}

DateTime *rtcGetClockTime(DateTime *dt) {
	memcpy(dt, &_state.clockTime, sizeof(_state.clockTime));

	return dt;
}

void rtcSetAlarmTime(const DateTime *dt) {
	memcpy(&_state.alarmTime, dt, sizeof(_state.alarmTime));

	// In order to simulate the triggering of the alarm, we set alarmRaised
	// when the clock and the alarm have identical time portions.
	if (_state.clockTime.hour == _state.alarmTime.hour
		&& _state.clockTime.minute == _state.alarmTime.minute
		&& _state.clockTime.second == _state.alarmTime.second) {
		_state.alarmRaised = true;
	}
}

DateTime *rtcGetAlarmTime(DateTime *dt) {
	memcpy(dt, &_state.alarmTime, sizeof(_state.alarmTime));

	return dt;
}

void rtcEnableAlarm(bool enabled) {
	_state.alarmEnabled = enabled;
}

bool rtcIsAlarmEnabled() {
	return _state.alarmEnabled;
}

bool rtcIsAlarmRaised() {
	bool rc = _state.alarmRaised;

	if (rc) {
		_state.alarmRaised = false;
	}

	return rc;
}

Notez qu'il y a un peu de logique dans ce mock, dans rtcSetAlarmTime(), afin de pouvoir simuler le déclenchement de l'alarme pour tester notre code. Ce genre de chose représente le degré d'intelligence maximum qu'on peut avoir dans un mock.

Voila, vous avez maintenant toutes les clés, à vous de développer la logique applicative en vous référant aux spécifications.


Copyright (c) 2025 Vincent DEFERT - Tous droits réservés