La mia domanda è incentrata sulla progettazione e il codice incluso in questa domanda ha lo scopo di chiarire il problema che sto vivendo. Sono interessato alle risposte a livello concettuale e non al codice per rispondere alla mia domanda.
Sfondo:
Quando si programmano i sistemi bare metal, è spesso necessario manipolare i bit nel registro mappato in memoria controllando il comportamento delle periferiche on-chip o esterne. Di solito, ci sono librerie software che definiscono questi bit e registri. Un esempio minimo potrebbe essere simile a questo:
#include <stdio.h>
#include <stdint.h>
/* Might be in some library like this: */
#define SOME_PERIPHERAL_BASE_ADDRESS 0x8fce0000
#define SOME_PERIPHERAL_REG1 (*((uint16_t*)(SOME_PERIPHERAL_BASE_ADDRESS + 0x8e)))
#define SOME_PERIPHERAL_REG1_AM (1 << 3)
#define SOME_PERIPHERAL_REG1_FOO (1 << 4)
#define SOME_PERIPHERAL_REG1_BAR (1 << 5)
/* User code: */
int main()
{
/* How to use these definitions on a bare metal system: */
/* SOME_PERIPHERAL_REG1 is initialies to the reset value by hardware, so
* there is no need to initialise it. */
/* printf("reg1 = %i\n", SOME_PERIPHERAL_REG1); */
/* This will of course segfault when not on a bare metal system. */
/* SOME_PERIPHERAL_REG1 = SOME_PERIPHERAL_REG1_AM | SOME_PERIPHERAL_REG1_BAR; */
/* printf("reg1 = %i\n", SOME_PERIPHERAL_REG1); */
/* Ersatz dummy code for testing on a pc: */
uint16_t value=0;
printf("reg1 = %i\n", value);
value = SOME_PERIPHERAL_REG1_AM | SOME_PERIPHERAL_REG1_BAR; /* <- No type-safty! */
printf("reg1 = %i\n", value);
return 0;
}
Problemi con questo approccio:
Sebbene questo codice sia ragionevolmente leggibile e breve, manca in certi modi:
- Un programmatore sarà noioso dover ridigitare il prefisso (SOME_PERIPHERAL_REG1_ nell'esempio) per ogni valore di registro
- Nessuna sicurezza di tipo: un programmatore può facilmente impostare un registro su un valore destinato a un altro registro (ad esempio, impostare reg1 di Some_peripheral su un valore destinato al registro di controllo di Another_peripheral) o non su un valore di registro (ad es. imposta reg1 su 15 (un intero)).
- Un altro problema con questo approccio è che alcuni registri non sono accessibili con assegnazioni semplici. Ad esempio, utilizzo un controller video SSD1289 collegato all'interfaccia di memoria esterna del mio microcontrollore. Per impostare i registri dell'SSD1289, dovrò prima scrivere l'indirizzo del registro su un determinato indirizzo nell'area di memoria seguito da una scrittura del nuovo valore di registro su un altro indirizzo in quella regione. Con la semplice C, non c'è modo di incapsulare questo tranne che usando una funzione setter.
Soluzione potenziale:
Per superare questo, ho provato a creare un oggetto registro in C ++ che è parametrizzato con un enum contenente le definizioni bit e un puntatore all'indirizzo del registro. Un esempio minimo potrebbe essere simile a questo:
#include <iostream>
#include "enum_binary_operators.hpp"
// Code that should go in the library:
/**
* \brief An object ancapsulating a memory-mapped peripheral control register.
*
* \param reg_bits An enum defining the bitfields for that register.
* \param ret_address The memory-mapped address where the register lies in
* memory
*/
template < typename reg_bits, uintptr_t reg_address >
class Register
{
private:
// On the intended pare metal target, this wouldn't be neccesary, as
// the value would be stored in the memory-mapped address of the register.
uint16_t value = 0;
public:
// Write a new value to the register
inline Register& operator= (reg_bits const& val)
{
// This is what is intended, but of course segfaults when not on bare metal.
//*reinterpret_cast<uint16_t*>(reg_address) = static_cast < uint16_t > (val);
// dummy replacement of the above line.
value = static_cast < uint16_t > (val);
// possibly do some more things like send the value to a remote register.
return *this;
}
operator uint16_t() // Read register
{
// This is what is intended, but of course segfaults when not on bare metal.
//return *reinterpret_cast<uint16_t*>(reg_address);
// dummy replacement of the above line.
return value;
}
[[deprecated("Please only use the appropriate register bit enum values to manipulate the register!")]] Register& operator= (int val) {}
};
/**
* \brief An Object encapsulating a memory-mapped peripheral
*
* \param base_address The base address of the registers belonging to that peripheral.
*/
template < uintptr_t base_address>
class Some_peripheral
{
public:
// This enum should not be in global namespace to avoid name conflicts.
// For example many peripherals might have a register named
// "control_register".
enum reg1_bits : uint16_t
{
AM = (1 << 3),
FOO = (1 << 4),
BAR = (1 << 5),
};
// more register bit definitions
Register < reg1_bits, base_address + 0x8e > reg1;
// more registers
// ...
// possibly more high-level functions for the peripheral like
// initialisation routines, transmission routines, etc.
};
// More peripherals
// The base address should be provided by some file defining the peripheral addresses for a given chip.gg
//extern constexpr some_peripheral_base;
constexpr uintptr_t some_peripheral_base = 0x8fce0000;
Some_peripheral<some_peripheral_base> some_peripheral1;
// And some more peripherals
// User code:
int main()
{
// Get the current value of the register.
std::cout<<"reg1 = "<<static_cast < uint16_t > (some_peripheral1.reg1)<<std::endl;
// Each register can only be written with the bits that belong to it.
some_peripheral1.reg1 = Some_peripheral<some_peripheral_base>::reg1_bits::AM
| Some_peripheral<some_peripheral_base>::reg1_bits::BAR;
// This is the intended usage:
// Somehow, operator= should be able to deduce the scope resolution automatically at runtime.
//some_peripheral1.reg1 = AM | BAR;
std::cout<<"reg1 = "<<static_cast < uint16_t > (some_peripheral1.reg1)<<std::endl;
return 0;
}
Con enum_binary_operators.hpp:
#ifndef ENUM_BINARY_OPERATORS_HPP
#define ENUM_BINARY_OPERATORS_HPP
#include <type_traits>
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator|(T const& x, T const& y) {
return static_cast < T > (static_cast < uint16_t > (x) | static_cast < uint16_t > (y));
}
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator&(T const& x, T const& y) {
return static_cast < T > (static_cast < uint16_t > (x) & static_cast < uint16_t > (y));
}
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator^(T const& x, T const& y) {
return static_cast < T > (static_cast < uint16_t > (x) ^ static_cast < uint16_t > (y));
}
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator<<(T const& x, T const& y) {
return static_cast < T > (static_cast < uint16_t > (x) << static_cast < uint16_t > (y));
}
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator<<(unsigned int const x, T const& y) {
return static_cast < T > (x << static_cast < uint16_t > (y));
}
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator<<(int const x, T const& y) {
return static_cast < T > (x << static_cast < uint16_t > (y));
}
#endif /* ENUM_BINARY_OPERATORS_HPP */
Domande:
Ora le mie domande sono:
- Sono corretto nel presupporre che questo dovrebbe arrivare a un tempo di esecuzione trascurabile? Ovviamente, il tempo di compilazione verrà aumentato.
- C'è un modo migliore per farlo?
- E c'è un modo per sbarazzarsi dei lunghi specificatori di scope nel compito? L'idea è qui, che il compilatore sa già, da dove i membri dell'enum dovranno venire (dato che l'enum è un argomento template per la classe register), quindi forse si potrebbe ottenere che includa la corretta risoluzione dell'ambito da sola in in qualche modo.