ciaa / firmware_v2

CIAA-Firmware v2.0.0 development
BSD 3-Clause "New" or "Revised" License
18 stars 32 forks source link

Cosas generales que aplican a todos los módulos #24

Open epernia opened 7 years ago

epernia commented 7 years ago

Traigo la discusión de los mails:

Martín: sAPI es un modulo de nivel superior, en todo caso, los .c que son dependientes de cada arquitectura (y un .h privado que deberia ser transparente al usuario) son lo dependiente de cada arquitectura/placa. sAPI arriba o como parte de cada arquitectura? Es bastante complicada la primera, pero indudablemente es la mejor.

Ernesto: Comparto lo que dice martin del nivel chip y board, por otro lado no entiendo algunas cosas de como esta armada la arquitectura de los archivos, porque por ejemplo el modulo "sapi" esta dentro de modules/lpc4337_m4 no deberia estar a la altura de "modules" solamente? porque digamos que va a haber una sola lib para todos los micros.

Eric:

La sAPI actual es lo que fue saliendo de los cursos CAPSE y hay mucho dependiente del hardware aún. En el caso particular de HMC883L donde dice I2C0 justo LPCOpen usa ese mismo nombre, pero la idea es que cada periférico tenga varios (cantidad según la placa, e incluso podría haber por soft, en I2C hay por ejemplo).

La idea es que una placa tenga:

IO0 ... IOn AI0 ... AIn AO0 ... AOn UART0 ... UARTn

y así...

En la EDU-CIAA hay un monton de pines con nombres copados de los periféricos que trae pero es un garrón los pines que se llaman GPIOn porque confunde que no tiene nada que ver con los GPIOs posta que conectan y peor aún los que dicen LCDn o T_FILn, me re costó hacer entender a los pibes de la fafu y CAPSE que es simplemente una serigrafía.

Por eso la sAPI tenia primero nombres como los de arriba pero me traía el otro bardo que era decirle siempre a los pibes como mapeaba la sAPI los nombres esos contra la serigrafía de la placa... y era un embole, aunque les armé imágenes copadas siempre preguntan "a que pin va?" y por eso en esta última versión les puse los nombres de la placa. Por todo esto, creo que lo más sano es que la biblioteca tenga ambos nombres y que haya ejemplos para cada placa separados, los cuales sean generados automáticamente y les ponga los nombres de cada placa según serigrafía. Hay nombres que no están en serigrafía y de allí que se usen directo los genéricos.

Martín:

Conviene tener "metodos". Definitivamente es un problema tener "pocos" metodos.

Esa fue la experiencia de Trolltech con Qt2/Qt3 y por la que Qt4 necesitó una reescritura fundamental y un replanteo violento de los parametros de diseño.

Me explico, es comun creer que tener muchas funciones es un problema y hay que resolverlo con pocas funciones y muchos parametros. Lo que nos dice esto, es que tenemos una complejidad inherente grande.

A lo que voy es que, hacer:

periphInit(PERIPH1, param1, param2, param3, flag1|flag2|flag3|flag4);

Es peor a hacer:

periphInit(PERIPH1);
periphSet1(PERIPH1, param1);
periphSet2(PERIPH1, param2);
periphSet3(PERIPH1, param3);
periphSet4(PERIPH1, flag1|flag2|flag3|flag4);

En el primero, sea cual sea la necesidad de inicialización, hay que conocer todos Y CADA UNO de los parametros y su side-efect. En la segunda forma, el init lo deja en un estado conocido (init o config, da igual) mientras que el resto de las funciones lo modifican a la necesidad particular.

Normalmente, la config. por defecto, es cercana al mayor uso. Si se cambia algo, debe hacerse pocas veces y bastante acotado. Si la configuracion es muy distinta al formato por defecto, es que estamos haciendo algo "raro" y posiblemente requiera un approach mas fino en un contexto de gente que sabe en profundidad lo que hace.

En cuanto a la cantidad de funciones, es correcto lo que dice Eric sobre la cantidad, pero restringirnos a "Init", "Read" y "Write" es como minimo un problema serio porque transferimos la complejidad escencial del manejo de perifericos a los parametriso aunque se la quitamos a los nombres.

El ejemplo tipico de Qt sobre esto es QPushButton de Qt3 vs el de Qt4: https://wiki.qt.io/API_Design_Principles#Avoiding_Common_Traps

(reprodusco aca para el que no quiera seguir el link)

Versus Qt3:

QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical, 0, "volume");

Versus Qt4:

QSlider *slider = new QSlider(Qt::Vertical);
slider->setRange(12, 18);
slider->setPageStep(3);
slider->setValue(13);
slider->setObjectName("volume");

Creo que se entiende el punto, la primera tiene siete parametros totalmente OSCUROS mientras que el segundo codigo es autoexplicativo.

Ademas, si tenemos el mecanismo Set(...), Get(...) es totalmente nemotecnico y muy facil de recordar (dos funciones se transforman en un solo caso a recordar)

Eric:

Entonces estoy definiéndome la clase "Módulo" que tiene "valores posibles de configuración" y "funciones". esas funciones se componen de parámetros y un cuerpo de función en C que se lo pueden pedrir a una board particular.

Board tiene las partes de código dependientes del hard (chip+routing). Entonces armando en objetos ese modelo puedo generar los archivos en C de la biblio.

La idea es primero escribir todos los modos posibles de funcionamiento que queremos soportar (va a haber placas que puedan y algunas que no, donde se aclarará en el port de sAPI para esa placa y otras que tengan muchos más modos que no soportemos).

Por todo esto creo que tenemos que definir una placa abstracta (que en principio podría coincidir con la EDU-CIAA-NXP). En ella tendremos muchos periféricos GPIO (refiríendonos a un pin en particular) 1 ADC con 3 CHANNELs, 3 UARTS (USB 232 y 485), 1 SPI y 1 I2C en principio.

Martín:

No lo veo tan claro a eso, el board no es el componente base del que parte todo, sino el SoC que es una agrupación de IPCores.

Y son los IPCores los que tenemos que modelar de alguna forma, luego, la placa surge sola como especialización/combinación de esos IPCore.

La placa es irrelevante, importa el SoC y sus IPCores. En todo caso, hay que plantear un API de acceso a IPCores genericos. En el caso que nos ocupa, las interfaces a definir minimas serian: GPIO, SPI, I2C, ADC, DAC, UART y TIMERS

Eso de forma generica para cualquier SoC. Luego tenemos en el mapping los distintos IPCores (I2C0, I2C1, TIM0, TIM1, TIMn, SPI0, SPI1, etc, etc) que podran o no existir en algunos chips. La idea es que empiece numerandose como 0, 1, 2.... entonces, si el SoC no tiene I2C no existirá ni siquiera I2C0, o si solo existen dos SPI, SPI3 no estará definido (por eso la idea de usar defines y así poder hacer #ifdef n para habilitar codigo compatible con una cosa u otra)

Eric: Cada módulo además tiene que saber de cierta placa los pines que tiene en uso, para que después tire error de config o algo si los mismos los quiere usar otro módulo, por ejemplo tento el Pin de la EDU SPI_MISO ocupado por el periférico DIO25 configurado como salida.

Martín: Creo que mas bien estas intentado resolver un problema de diseño con checking runtime. Eso podria ser posible en c++ con los templates, pero en C la única forma de hacerlo es escribir un lenguaje propio con el preprocesador de C o un lenguaje propio como OIL o M4, lo cual no recomiendo para nada.

Si alguien configura SPI0 (por ejemplo) y luego configura el GPIOx que se superpone con SPI0_MOSI, es problema de quien programa (se me ocurre mas de un caso donde ese conflicto es el comportamiento deseado, por ejemplo generar un error controlado en el protocolo)

Eric: Si, tenés razón esto no se tendría que resolver en runtime. Quizá se puede armar un "configurador" en el IDE que marque los recurso utilizados.

Martín: Si se puede configurar y poner a andar, no es un error, es una forma de usarlo, valida o invalida, pero no tenemos que capar el funcionamiento del sistema por esto. Creo que eso seria una feature de tier2 como mucho, y no le pondria esfuerso en lo mas minimo.

Eric: Estoy de acuerdo con que hay una dificultad intrínseca que tenemos que ver como afrontamos pero cuando una API tiene un número gigante de funciones asusta al usuario que recién arranca.

Martín: Ahi el problema es la forma de presentar el API al usuario. Si el usuario no sabe nada de programación, se va a sentir intimidado ya sea que vea 300 o 3 funciones.

De hecho, lo mismo ocurre si hay tres funciones pero con 300 parámetros cada una y con valores "magicos" sacados del traste para los que solo el que la diseño tiene explicación.

Eric: Viendo lo que dice Martín creo que la mejor solución de compromiso es hacer una función "config" que tenga como parámetros ENABLE o DISABLE del periférico (mejorando lo que está) y algún parámetro típico del mismo (por ejemplo baud rate). Luego si los get y set porperty para configurar los demás parámetros.

Separando así ganamos el modo simple y sencillo (de allí la s de sAPI) y luego un usuario que pide más cosas puede hacer set y get de cada property para una configuración avanzada.

Martín:

Me vuelvo a remitir al ejemplo del constructor de QSlider de Qt3 vs Qt4. El problema no es la cantidad de funciones, sino que este claro su propósito y no haya muchas formas de hacer lo mismo.

No se cual es la experiencia particular de cada uno aca con docencia, pero te puedo asegurar que el criterio de evaluar que tan facil o difícil le resulte a un principiante un API es cualquier cosa menos adecuado para determinar si algo es bueno o malo.

Fijate que en la dificultad que encuentre un principante puede enmasararse las bases previas, cuantos malos conceptos arrastre, la disposición del momento e innumerables cosas mas que, en otras circunstancias te habrian dado un resultado distinto.

O sea, es imposible saber si la cantidad de funciones asusta a un principante, solo por la forma de presentarlas, por la base previa que traigan, por el ambiente y el planteamiento del problema o porque realmente tener muchas funciones indica algo "complicado".

De hecho, hay un parametro bastante universal y bien estudiado y falsable que nos dice que el cerebro humano a partir de los 4-7 años puede concentrar su atención en 5-7 elementos agrupados (de ahi que los power ranger sean cinco y no 23)

Ese parametro que comento es una regla de oro para el desarrollo de APIs y librerias. Los bloques logicos se agrupan en elementos de 5 o 7 (a lo sumo 8, ya si se va a 9-10 es factible que sea complicado)

https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus_or_Minus_Two

http://www.psych.utoronto.ca/users/peterson/psy430s2001/Miller%20GA%20Magical%20Seven%20Psych%20Review%201955.pdf

Si bien el articulo original abla de entidades, tengamos en cuenta que esas entidades pueden ser paquetes.

Como seria?

<periph>Config(name, ENABLE|DISABLE, <minimal params>)????

Creo que la semantica se rompe en el caso de disable:

coreConfig(CORE1, DISABLE, WTF???? Que pongo aca??? Porque necesito parametros para deshabilitarlo????);

Yo apoyo mas la idea de:

coreEnable(CORE1);
coreConfig(CORE1, param1, param2);

coreSetParam3(CORE1, param3);
coreSetParam2(CORE1, coreGetParam2(CORE1) | MODIFY_FLAG);

...uso de CORE1...

coreDisable(CORE1);

Pero creo que seria todavia mejor que nos abstraigamos de coreEnable/coreDisable, no aplica a todos los perifericos, no aporta nada al uso y se puede enmascarar perfectamente en la llamada a la funcion de config o cualquiera de las subsecuentes.

Si hay que apagarlo, yo diria tener dos propiedades mas como

coreSetPower(CORE, ON | OFF);

Pero no hacerlo universal para todos los perifericos...

Eric:

Esto no me queda claro aún como conviene resolverlo, pero slavo los GPIO está bueno poder apagar y encender periféricos para consumir menos. Inclusive según el micro recomiendan los GPIOs no usados ponerlos en tal configuración para que consuma menos, así que esa podría ser nuestra disable u off.

Martín:

Fijense que esto es programación orientada a objetos 1, tenemos el IP Core (una clase) que tiene varias instancias (todas predefinidas) y tenemos varios metodos para acceder a esos cores. Esto pide a gritos algo como:

typedef enum {
   <class data>
} core_class_t;

void core_init(core_class_t *_this, ....);

No hace falta destructores porque los objetos existen fisicamente durante TODO el tiempo del programa

ret_type core_method1(core_class_t *this, param1, param2, paramN);
ret_type core_method2(core_class_t *this, param1, param2, paramN);
ret_type core_method3(core_class_t *this, param1, param2, paramN);
....
ret_type core_methodN(core_class_t *this, param1, param2, paramN);

extern core_class_t _core_instance_1;
extern core_class_t _core_instance_2;
...
extern core_class_t _core_instance_N;

Y si no queremos usar la forma core_methodN( &instance, ...) usamos los defines:

#define CORE_INSTANCE1 (&_core_instance_1)
#define CORE_INSTANCE2 (&_core_instance_2)
...
#define CORE_INSTANCEN (&_core_instance_N)

Eric: Estoy de acuerdo con la abstracción de IP Core (sería mi equivalente a módulo), sin embargo tenemos que tener en paralelo la de board con sus conectores, borneras y tiras de pines para asociar los periféricos a ciertos pines o conectores. De esta manera en general podríamos decir que por tal conector o pin sale el perifériico A o el B.

Martín: No es excluyente lo uno con lo otro. Yo lo que planteo es que basarnos en el board es poner el carro antes del caballo. Por supuesto, el API no esta terminado sin una capa de abstracción que te ayude a identificar los pines en la placa.

Eric: Hay un tema de implementación que tenemos que si conviene, o no, usar los if def, esto lo haría más con makefiles porque hay varias carpetas para cada arquitectura. Incluso quizá haya que agregar carpetas para diferentes placas en la estructura de carpetas de Pablo (tenemos 2 placas con el LPC4337).

Martín: Eso hay que hacerlo independientemente de lo que digo arriba de los ipcore. De todas formas, es un asunto menor, en todo caso, cuando hagamos referencia a un periferico que no existe en una arquitectura particular (por ejemplop i2c4 cuando solo hay tres) el compilador chillara al no encontrar el nombre. Idealmente, los perifericos deben poder referirse con el nombre del ipcore (I2C0, SPI3, UART3) y/o con el nombre asociado al BSP (board package support) como UART_RS485 en la edu-ciaa.

Eric:

Onda objetos están hechos los módulos Delay, Display7Segment, Kyepad, que son en general los que tienen que mantener algun atributo más que su nombre (GPIO0 por ejemplo).

Los que solo necesitaba el nombre y los atributos los mantiene guardados por hardware no le puse structs para evitar los punteros. Se puede tener un "mirror" por software de la configuración mediante una estructura.

Lo de tener ilimitada cantidad de instancias lo dejaría para periféricos de soft que no sean fijos, para los que son fijos se puede evitar la estructura porque sabemos de antemano cuantos va a tener cada placa, entonces con el nombre s epuede buscar su estructura asociada.

Esto de usar estructuras viene bien cuando no sabemos cuantos va a usar el usuario de tal módulo y no queremos usar memoria dinámica (intento que la biblioteca no use para nada memoria dinámica).

Martín:

Es un ejemplo de programación orientada a objetos, en el caso de los ipcore, la struct es el área donde se encuentra mapeado el ipcore y no es accesible desde el API publica mas alla de como un indice con nombre (I2C0=0, SPI1=1, UART4=3, etc)

Los IPCore con existencia fisica, no son "creables" sino "iniciables", ademas, tienen definidos nombres prefijados que no podemos destruir o crear dos veces. Es decir, no podemos tener 10 SPI apuntando al mismo elemento fisico, SPI0, es SPI0 desde que el software arranca hasta que termina o se destrulle el chip.

En cuanto a los periféricos "virtuales" como un chip conectado al I2C o al SPI, se les deben asignar structs específicas porque no existen hasta que se "crean" e "inician" (análogo al new de C++)

Resumiendo, para GPIO, I2C, SPI, UART y los periféricos físicos, basta con que el nombre asignado por el CORE/BSP, para uno virtual, hay que crear su espacio en memoria (con struct eepromi2c_t eeprom1; por ejemplo), luego iniciarlo y finalmente usarlo.

epernia commented 7 years ago

@martinribelotta: En estos documentos describo lo de esta discusión más formalizado: