
Buena salud a todos!
En un artículo anterior, prometí escribir sobre cómo puedes trabajar con una lista de puertos.
Debo decir de inmediato que todo ya estaba decidido antes de mí en 2010, aquí está el artículo: Trabajar con los puertos de entrada / salida de microcontroladores en C ++ . La persona que escribió esto en 2010 es simplemente guapo.
Estaba un poco avergonzado de que haría lo que ya se había hecho hace 10 años, así que decidí no esperar hasta 2020, pero hacerlo en 2019 para repetir la decisión hace ya 9 años, no será tan tonto.
En el artículo anterior, el trabajo con listas de tipos se realizó utilizando C ++ 03, cuando más plantillas tenían un número fijo de parámetros y las funciones no podían ser expresiones constexpr. Desde entonces, C ++ ha cambiado un poco, así que tratemos de hacer lo mismo, pero en C ++ 17. Bienvenido a cat:
Desafío
Por lo tanto, la tarea es instalar o eliminar varios pines del procesador a la vez, que se combinan en una lista. Los pines se pueden ubicar en diferentes puertos, a pesar de esto, dicha operación debe realizarse de la manera más eficiente posible.
En realidad, lo que queremos hacer se puede mostrar con el código:
using Pin1 = Pin<GPIO, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; int main() {
Sobre el registro BSRRPara aquellos que no conocen los asuntos del microcontrolador, el GPIOA->BSRR
es responsable de la instalación atómica o el restablecimiento de los valores en las patas del microcontrolador. Este registro es de 32 bits. Los primeros 16 bits son responsables de establecer 1 en las patas, los segundos 16 bits para establecer 0 en las patas.
Por ejemplo, para establecer el tramo número 3 en 1, debe establecer el tercer bit en 1 en el registro BSRR
. Para restablecer el tramo número 3 en 0, debe establecer 19 bits en 1 en el mismo registro BSRR
.
Un esquema generalizado de pasos para resolver este problema se puede representar de la siguiente manera:

Bueno, en otras palabras:
Para que el compilador lo haga por nosotros:
- Verifique que la lista contenga solo un Pin único
- crear una lista de puertos determinando en qué puertos está el Pin,
- calcular el valor que se colocará en cada puerto
Y luego el programa
Y debe hacer esto de la manera más eficiente posible, de modo que incluso sin optimización, el código sea mínimo. En realidad esta es toda la tarea.
Comencemos con la primera moda: verificar que la lista contiene Pin único.
Verifique la lista para la unicidad
Déjame recordarte que tenemos una lista de Pines:
PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> ;
Sin darse cuenta puede hacer esto:
PinsPack<Pin1, Pin2, Pin3, Pin4, Pin1> ;
Me gustaría que el compilador detectara ese error e informara al pianista al respecto.
Verificaremos la lista para ver si es única:
- Desde la lista de origen, cree una nueva lista sin duplicados,
- Si el tipo de la lista fuente y el tipo de la lista sin duplicados no coinciden, entonces el PIN era el mismo en la lista fuente y el programador cometió un error.
- Si coinciden, entonces todo está bien, no hay duplicados.
Para crear una nueva lista sin duplicados, un colega aconsejó no reinventar la rueda y adoptar el enfoque de la biblioteca Loki. Tengo este enfoque y robé. Casi lo mismo que en 2010, pero con un número variable de parámetros.
El código que tomó prestado un colega que tomó prestada la idea de Loki namespace PinHelper { template<typename ... Types> struct Collection { };
¿Cómo se puede usar esto ahora? Sí, es muy simple:
using Pin1 = Pin<GPIOC, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; using Pin6 = Pin<GPIOC, 1>; int main() {
Bueno, es decir si configura incorrectamente la lista de pines, y accidentalmente se indican dos pines idénticos en la lista, el programa no se compilará y el compilador dará un error: "Problema: los mismos pines en la lista".
Por cierto, para garantizar la lista correcta de pines para puertos, puede utilizar el siguiente enfoque: Ya hemos escrito mucho aquí, pero hasta ahora no hay una sola línea de código real que ingrese al microcontrolador. Si todos los pines están configurados correctamente, el programa de firmware tiene este aspecto:
int main() { return 0 ; }
Agreguemos un poco de código e intentemos que el método Set()
establezca los pines en la lista.
Método de instalación de pin de puerto
Avancemos un poco hasta el final de la tarea. En última instancia, es necesario implementar el método Set()
, que automáticamente, según el Pin de la lista, determinaría qué valores se deberían instalar en qué puerto.
El codigo que queremos using Pin1 = Pin<GPIOA, 1>; using Pin2 = Pin<GPIOB, 2>; using Pin3 = Pin<GPIOA, 2>; using Pin4 = Pin<GPIOC, 1>; using Pin5 = Pin<GPIOA, 3>; int main() { PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ;
Por lo tanto, declaramos una clase que contendrá una lista de Pines, y en ella definimos el método público estático Set()
.
template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; public: __forceinline static void Set(std::size_t mask) { } } ;
Como puede ver, el método Set(size_t mask)
toma algún tipo de valor (máscara). Esta máscara es el número que necesita poner en los puertos. Por defecto, es 0xffffffff, lo que significa que queremos poner todos los pines en la lista (máximo 32). Si pasa otro valor allí, por ejemplo, 7 == 0b111, solo se deben instalar los primeros 3 pines de la lista, y así sucesivamente. Es decir máscara superpuesta en la lista Pin.
Para poder instalar cualquier cosa en los pines, necesita saber en qué puertos están estos pines. Cada Pin está vinculado a un puerto específico y podemos extraer estos puertos de la clase Pin y crear una lista de estos puertos.
Nuestros pines están asignados a diferentes puertos:
using Pin1 = Pin<Port<GPIOA>, 1>; using Pin2 = Pin<Port<GPIOB>, 2>; using Pin3 = Pin<Port<GPIOA>, 2>; using Pin4 = Pin<Port<GPIOC>, 1>; using Pin5 = Pin<Port<GPIOA>, 3>;
Estos 5 pines tienen solo 3 puertos únicos (GPIOA, GPIOB, GPIOC). Si declaramos una lista de PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>
, entonces necesitamos obtener una lista de tres puertos: Collection<Port<GPIOA>, Port<GPIOB>, Port<GPIOC>>
La clase Pin contiene el tipo de puerto y en una forma simplificada se ve así:
template<typename Port, uint8_t pinNum> struct Pin { using PortType = Port ; static constexpr uint32_t pin = pinNum ; ... }
Además, aún necesita definir una estructura para esta lista, será solo una estructura de plantilla que tomará un número variable de argumentos de plantilla
template <typename... Types> struct Collection{} ;
Ahora definimos una lista de puertos únicos y, al mismo tiempo, verificamos que la lista de pines no contenga los mismos pines. Esto es fácil de hacer:
template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private:
Adelante ...
Bypass de lista de puertos
Después de recibir la lista de puertos, ahora debe omitirla y hacer algo con cada puerto. De forma simplificada, podemos decir que debemos declarar una función que recibirá una lista de puertos y una máscara para la lista de pines en la entrada.
Como debemos omitir una lista cuyo tamaño no se conoce con certeza, la función será una plantilla con un número variable de parámetros.
Iremos "recursivamente", aunque todavía hay parámetros en la plantilla, llamaremos a una función con el mismo nombre.
template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: __forceinline template<typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask) {
Entonces, aprendimos cómo omitir la lista de puertos, pero además de la omisión, debe hacer un trabajo útil, a saber, instalar algo en el puerto.
__forceinline template<typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask) {
Este método se ejecutará en tiempo de ejecución, ya que el parámetro de mask
se pasa a la función desde afuera. Y debido al hecho de que no podemos garantizar que se pasará una constante al método SetPorts()
, el método GetValue()
también comenzará a ejecutarse en tiempo de ejecución.
Y aunque, en el artículo Trabajando con puertos de entrada / salida de microcontroladores en C ++, está escrito que en un método similar el compilador determinó que se pasó una constante y calculó el valor para escribir en el puerto en la etapa de compilación, mi compilador hizo tal truco solo con la máxima optimización.
Me gustaría que GetValue()
ejecute en tiempo de compilación con cualquier configuración del compilador.
No encontré en el estándar cómo el compilador debe liderar al compilador en este caso, pero a juzgar por el hecho de que el compilador IAR hace esto solo al máximo nivel de optimización, lo más probable es que no esté regulado por el estándar o no debe tomarse como una expresión constexpr.
Si alguien lo sabe, escriba los comentarios.
Para garantizar la transferencia explícita de un valor constante, crearemos un método adicional con mask
paso en la plantilla:
__forceinline template<std::size_t mask, typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>) { using MyPins = PinsPack<Ts...> ;
Por lo tanto, ahora podemos revisar la lista de Pines, extraer los puertos de ellos y hacer una lista única de puertos a los que están vinculados, y luego revisar la lista de puertos creada y establecer el valor requerido en cada puerto.
Queda por calcular este valor .
Cálculo del valor que debe establecerse en el puerto
Tenemos una lista de puertos que obtuvimos de la lista Pin, por ejemplo, esta es una lista: Collection<Port<GPIOA>, Port<GPIOB>, Port<GPIOC>>
.
Debe tomar un elemento de esta lista, por ejemplo, el puerto GPIOA, luego, en la lista de pines, encuentre todos los pines que están conectados a este puerto y calcule el valor para la instalación en el puerto. Y luego haz lo mismo con el próximo puerto.
Una vez más: en nuestro caso, la lista de pines para obtener una lista de puertos únicos es la siguiente: using Pin1 = Pin<Port<GPIOC>, 1>; using Pin2 = Pin<Port<GPIOB>, 1>; using Pin3 = Pin<Port<GPIOA>, 1>; using Pin4 = Pin<Port<GPIOC>, 2>; using Pin5 = Pin<Port<GPIOA>, 3>; using Pins = PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> ;
Entonces, para el puerto GPIOA, el valor debe ser (1 << 1 ) | (1 << 3) = 10
(1 << 1 ) | (1 << 3) = 10
, y para el puerto GPIOC - (1 << 1) | (1 << 2) = 6
(1 << 1) | (1 << 2) = 6
, y para GPIOB (1 << 1 ) = 2
La función para el cálculo acepta el puerto solicitado y si Pin está en el mismo puerto que el puerto solicitado, entonces debe establecer el bit correspondiente a la posición de este Pina en la lista, uno (1) en la máscara.
No es fácil de explicar con palabras, es mejor mirar directamente en el código:
template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: __forceinline template<class QueryPort> constexpr static auto GetPortValue(std::size_t mask) { std::size_t result = 0;
Establecer el valor calculado para cada puerto en puertos
Ahora sabemos el valor que debe establecerse en cada puerto. Queda por completar el método público Set()
, que será visible para el usuario para que toda esta economía se llame:
template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; __forceinline static void Set(std::size_t mask) {
Como en el caso de SetPorts()
un método de plantilla adicional para garantizar la transferencia de la mask
como una constante, pasándola en el atributo de plantilla.
template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ;
En forma final, nuestra clase para la lista Pin se verá así: using namespace PinHelper ; template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: using TPins = typename NoDuplicates<Collection<Ts...>>::Result; static_assert(std::is_same<TPins, Collection<Ts...>>::value, ": ") ; using Ports = typename NoDuplicates<Collection<typename Ts::PortType...>>::Result; template<class Q> constexpr static auto GetPortValue(std::size_t mask) { std::size_t result = 0; auto rmask = mask ; pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0) & mask) * (1 << Ts::pin), mask>>=1)...}; pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0) & ~rmask) * ((1 << Ts::pin) << 16), rmask>>=1)...}; return result; } __forceinline template<typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask) { auto result = GetPortValue<Port>(mask) ; Port::Set(result & 0xff) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template SetPorts<Ports...>(Collection<Ports...>(), mask) ; } } __forceinline template<std::size_t mask, typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>) { constexpr auto result = GetPortValue<Port>(mask) ; Port::Set(result & 0xff) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template SetPorts<mask, Ports...>(Collection<Ports...>()) ; } } __forceinline template<typename Port, typename ...Ports> constexpr static void WritePorts(Collection<Port, Ports...>, std::size_t mask) { auto result = GetPortValue<Port>(mask) ; Port::Set(result) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template WritePorts<Ports...>(Collection<Ports...>(), mask) ; } } __forceinline template<std::size_t mask, typename Port, typename ...Ports> constexpr static void WritePorts(Collection<Port, Ports...>) { Port::Set(GetPortValue<Port>(mask)) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template WritePorts<mask, Ports...>(Collection<Ports...>()) ; } } public: static constexpr size_t size = sizeof ...(Ts) + 1U ; __forceinline static void Set(std::size_t mask ) { SetPorts(Ports(), mask) ; } __forceinline template<std::size_t mask = 0xffffffffU> static void Set() { SetPorts<mask>(Ports()) ; } __forceinline static void Write(std::size_t mask) { WritePorts(Ports(), mask) ; } __forceinline template<std::size_t mask = 0xffffffffU> static void Write() { WritePorts<mask>(Ports()) ; } } ;
Como resultado, todo se puede usar de la siguiente manera:
using Pin1 = Pin<GPIOC, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; using Pin6 = Pin<GPIOA, 5>; using Pin7 = Pin<GPIOC, 7>; using Pin8 = Pin<GPIOA, 3>; int main() {
Un ejemplo más completo se puede encontrar aquí:
https://onlinegdb.com/r1eoXQBRH
Rendimiento
Como recordará, queríamos que nuestra llamada se convirtiera a 3 líneas, configurada en el puerto A 10, el puerto B - 2 y el puerto C - 6
using Pin1 = Pin<GPIO, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; int main() {
Veamos qué sucedió con la optimización desactivada por completo.

Teñí los valores de los puertos y las llamadas para establecer estos valores en puertos en verde. Se puede ver que todo se hace según lo previsto, el compilador para cada uno de los puertos calculó el valor y simplemente llamó a la función para establecer estos valores en los puertos requeridos.
Si las funciones de instalación también se hacen en línea, al final recibimos una llamada para escribir el valor en el registro BSRR para cada puerto.
En realidad eso es todo. A quién le importa, el código está aquí .
Un ejemplo está aquí .
https://onlinegdb.com/ByeA50wTS