Quantcast
Channel: Blog MicroEmbebidos (PIC,MSP430,LPC,RTOS)
Viewing all 26 articles
Browse latest View live

Tutorial uC/OS-III y LPC1768 – Conceptos Básicos

$
0
0

Así como los Dioses Primigenios primero hicieron la luz, nosotros primero encenderemos un led…

Se suscita la primera pregunta, ¿es necesario un RTOS para encender un simple y vulgar led? La respuesta obviamente es no. Un RTOS es una herramienta para los programadores de microcontroladores y de hardware (sí, tambien ya está siendo aplicado a los FPGAs) la cual nos ayuda a implementar un sistema operativo de tiempo real para que gestione todas las tareas de nuestra aplicación. De ahi viene lo que significan sus siglas en inglés: Real Time Operative System o también conocidos como real time kernel.

Para entender su importancia describamos un escenario (extraído del libro del uC/OS-III):

Tú sistema tiene que leer un teclado y actualizar la pantalla lcd. Eso es muy fácil de manejar en un simple bucle.

Oh, espera, luego está el convertidor A/D, que requiere ser atendido cada 1 milisegundo. Encima los datos son ruidosos, así que debes realizar diez muestras para promediarlas y el resultado se ingresa en un cálculo que luego es enviado a la pantalla. Pero no puedes hacer los cálculos hasta que los resultados del encoder estén disponibles, y eso sólo se puede leer en intervalos de 20 milisegundos.

Y no se olvide de controlar la fuente de radiación, si se encuentra fuera de los límites se tiene que invocar a un protocolo de seguridad para evitar dañar a los usuarios. Eso tiene que ser controlado cada 250 milisegundos.

¿Cómo se podría escribir este código? Claro que es posible escribir un controlador de interrupciones que use los ticks del reloj del CPU, luego a través de una serie de torturantes loops hacer frente a las tareas propias de la aplicación. Los lazos se vuelven más complejos y el programa es cada vez más complicado lo que convierte al código en difícil de depurar y aun más difícil de mantener.

Es precisamente en estos escenarios donde la herramienta RTOS interviene para simplificar el trabajo del programador y así mismo hacer más eficiente el desempeño del código de nuestra aplicación.  Pero cómo es posible que un RTOS pueda hacer más fácil mi trabajo en una aplicacion con decenas de interfaces y servicios que tiene que atender aparentemente todo al mismo tiempo?

Para contestar a esta pregunta conozcamos las formas tradicionales de programar versus un RTOS. La forma primaria de programar una aplicación sencilla es mediante un Polled Loop que es una especie de lazo cerrado que espera que se cumpla alguna condición para recién realizar una acción. Por ejemplo si usamos el puerto UART de un micro pues podemos estar evaluando constantemente el flag de recepción para leer el buffer. Lo cual sirve siempre y cuando sea lo único que hay que hacer y no es eficiente cuando existen otras señales asíncronas que evaluar.

Luego está el método Polled Loop With Interrupts, o bucle repetitivo con interrupciones. Éste es el más usado por los duchos en microcontroladores (me incluyo XD), pues permite atender eventos asíncronos fuera del bucle repetitivo gracias a las interrupciones por hardware del propio microcontrolador. Sin embargo tambien presenta problemas cuando dentro de una interrupcion se gestiona un código que demora mucho y abarca todo el consumo del CPU. Por ejemplo si usamos la interrupcion de recepción del UART y en el main loop estamos evaluando la conversion A/D para mostrarla en pantalla, imaginen que sucedería si la recepción de datos serial detectara una trama de 1KB de información, entonces “quizás” perdamos conversiones debido a que el flujo del código estará entrampado en la interrupción. Tendríamos que asignarles tiempos límites a la interrupcion serial o manejar banderas que gestionen los efectos de las interrupciones en otro lado. Y que tal si así mismo tenemos que escuchar por otros canales de comunicación? Ya comienza el dolor de cabeza…

Afortunadamente desarrollaron el RTOS, el cual es cómo una plantilla de código sobre la cual plasmaremos nuestra aplicación organizada o dividida en tareas. Es decir lo que ingresaremos ahora serán tareas o tasks, las cuales serán gestionadas por el kernel (y configurada por nosotros) de tal forma que aparenta que todo sucede al mismo tiempo, es decir realiza el famoso multitasking con nuestras tareas.

Veamos estas imágenes, en la primera dice: todas las tareas parecen ejecutarse en simultáneo, observen las 3 tareas y sus líneas contínuas a lo largo del eje de tiempo. La segunda imagen devela lo que realmente está sucediendo: pero sólo una tarea es siempre ejecutada a la vez, esos segmentos de colores que pertenecen a cada tarea son las distribuciones de tiempo que realiza el kernel, esto es la multitarea.

Entonces el enfoque de programación va a cambiar, ya no nos preocuparemos por banderas, ni tiempos de espera, variables globales, variables locales, funciones, etc., de todo eso se encarga el kernel utilizando semáforos, colas de espera, y otros servicios propios de su naturaleza. Ahora podremos centrar nuestra atención completamente en la complejidad de la aplicación y su interacción con el mundo real que es donde se desempeñan los sistemas embebidos.

Dentro de la gran variedad de sistemas operativos en tiempo real que existen hasta la actualidad, he decidido trabajar con el uC/OS-III, ya que tiene una muy pero muy buena fuente de información además que es de acceso gratuito para fines académicos (como lo és nuestro blog), tiene una gran cantidad de ports (migraciones del código fuente) para muchos microcontroladores y está escrito en puro ANSI-C.

De los ports para este RTOS pienso usar el dedicado para el LPC1768 ya que existe un libro del uC/OS-III dedicado a este micro con arquitectura Cortex-M3 de la firma NXP. La otra buena noticia es que si aprendes a usar el uC/OS-III tranquilamente podrás entender otro RTOS como el FreeRTOS.

Para más información sobre uC/OS-III y micrium, de donde sale, porque es III? osea ya hubo un II? Quien es Jean Labrosse?, etc. pueden leer una entrevista que le hicieron allá por el 2011 cuando habían pasado dos años de haber hecho el lanzamiento del uC/OS-III:

Entrevista a Jean Labrosse

Creo que ya fue suficiente contexto histórico y me quede sin espacio para el prende led… así que la seguiremos en el siguiente post!

Henry Laredo Q.



Tutorial Pinguino – Capitulo 7. USB-CDC Emulación de Puerto Serie

$
0
0

Este tutorial trata sobre la comunicación USB – CDC (Class Device Communication) que permite emular a través del puerto USB un COM PORT virtual para acceder desde softwares que tengan implementado este tipo de comunicación.

En este tutorial exploraremos las funciones:

cantidadbytesleidos = CDC.read(&buffer[0]); –> Utilizado para leer datos del com port virtual. Esta función requiere de especificar como argumento de entrada un buffer donde almacenará los datos recibidos por el com port virtual a través del usb y nos retornorá la cantidad de datos leídos que han sido almacenados en el buffer especificado.

CDC.printf(“Cadena”); –> Utilizado para enviar datos al com port virtual. Esta función requiere que le carguemos una cadena de caracteres, así mismo hereda todas las propiedades del famoso “printf” por lo cual tambien permite enviar variables dandoles formato.

En este enlace pueden encontrar diversos ejemplos con el printf para conocer como formatear variables y las prueben con la función CDC.printf que existe en el pinguino.

Codigo:

char dato;
 unsigned char receivedbyte;
 unsigned char rxstr[64];
 #define led7 7</p>
void setup()
 {
 pinMode(led7, OUTPUT);
 digitalWrite(led7,LOW);
 }

void loop()
 {
 CDC.printf("Press a Key ...\r\n");

while((receivedbyte = CDC.read(rxstr)) == 0);

if(receivedbyte > 0)
 {
 dato = rxstr[0];
 CDC.printf("\r\nYou pressed \"%c\"\r\n", dato);

if(dato == '2')
 {
 digitalWrite(led7,HIGH);
 }
 else if(dato == '3')
 {
 digitalWrite(led7,LOW);
 }

}
 }

Video:

Novedad Driver Microchip USB CDC:

En algunas versiones del pinguino no han incluido la librería para utilizar el driver de microchip para la comunicación USB CDC. Dicho driver lo pueden descargar desde: 4Shared o Boxnet.

Henry Laredo Q.


Tutorial uC/OS-III y LPC1768 – Estructura Interna de Archivos

$
0
0

Continúo con los previos al tan esperado prende led XD, sucede que me pareció adecuado desarrollar este tema antes del primer proyecto ya que será la plataforma base para todos los ejemplos que haremos en esta sección de tutoriales. Entender la estructura interna del uC/OS-III es importante para conocer qué archivos son imprescindibles para construir una aplicación desde cero con RTOS y recién a partir de ahí agregar otros ficheros secundarios que realizen una misión específica o complementaria.

Directorios y archivos de una aplicación con uC/OS-III

En el libro uC/OS-III The Real Time Kernel encontraremos en la página 42 un diagrama de bloques de los archivos que utiliza una aplicación con este kernel. Sin embargo cuando la comparamos con uno de los ejemplos del libro para el LPC1768, hallaremos ciertas diferencias, y es que algunos directorios y archivos son agregados para permitir la realización de un port del kernel para la arquitectura CortexM3 y luego específicamente para el microcontrolador LPC1768. En estos cambios no hay un estándar definido, el que hace el port le pone lo que quiera.

Afortunadamente es posible identificar los archivos imprescindibles gracias a la información plasmada en el libro. Básicamente necesitamos lo siguiente:

EvalBoards es el directorio donde se encuentran dos directorios muy importantes, el Application Code (Ejemplo1) y el BSP (Board Support Package):

Observen que dentro de EvalBoards hay una serie de subcarpetas las cuales permiten tener en orden los ports para las aplicaciones que hagamos. La primera subcarpeta es el nombre del fabricante en este caso la firma que comercializa el LPC1768 es NXP, luego viene otra subcarpeta que refiere al nombre de la placa donde plasmaremos nuestra aplicación (los ejemplos del libro son con una placa de evaluación llamada MCB1700 de NXP) y una tercera subcarpeta que indica el IDE que será utilizado para contruir el código, en nuestro caso el KeilMDK .

En la carpeta BSP se alojan los archivos que hacen la interface entre el código de aplicación y las entradas y/o salidas de la placa que vamos a utilizar. Mientras que la carpeta Ejemplo1 es el lugar donde se aloja el archivo principal del proyecto, es decir donde es escribe el famoso “main()”.

En uC-CPU encontraremos funciones del fabricante para acceder a los periféricos del CPU en cuestión, como por ejemplo el manejo de interrupciones. Y en la subcarpeta Cfg hay un archivo llamado cpu_cfg.h que debe copiarse en la carpeta de la aplicación (en Ejemplo1).

En uC-CSP se encuentran archivos que hacen las veces de API que provee acceso por hardware hacia algunos periféricos del microcontrolador. CSP viene de Chip Support Package.

En uC-LIB encontraremos funciones comunes para copia de arrays, strings, manejo de ASCII, por ejemplo. Dentro de la subcarpeta Cfg hay un archivo llamado lib_cfg.h que debe debe copiarse en la carpeta de la aplicación (en Ejemplo1).

Y finalmente llegamos a la última carpeta y más importante de todas llamada uCOS-III que contiene el codigo fuente del kernel. Ahi dentro no debemos modificar ni una sola línea de código.

 

Esta estructura de directorios y archivos serán nuestra piedra angular para la contrucción de las aplicaciones que iremos implementando en los siguientes tutoriales.

Finalmente para terminar con este post, les dejo un video donde les muestro como armar esta plantilla desde cero en el uVision4:

Henry Laredo Q.


Tutorial uC/OS-III y LPC1768 – Prender un Led

$
0
0

Es el momento adecuado para prender un led con el uC/OS-III. En este post veremos como implementar un proyecto sencillo con dicho RTOS y el LPC1768. En simultáneo identificaremos lo que se necesita conocer en el microcontrolador para manipular los GPIO (general purpose input/output).

En resumen necesitamos entender lo siguiente:

  • Cómo implementar un proyecto con el uC/OS-III (ver este post).
  • Cómo crear un task (tarea) para el uC/OS-III (leanse todo el capitulo 3 del libro uC/OS-III for the NXP LPC1768).
  • Cómo manipular los pines de entrada/salida GPIO del LPC1768.
  • Cómo mapear los pines GPIO que voy a utilizar en el uC/OS-III.

Implementar un proyecto con el uC/OS-III

Dentro de la carpeta “test” (creado en el tutorial anterior) existe una sub carpeta EvalBoards, dentro existe otra sub carpeta llamada NXP que refiere al nombre del fabricante o vendedor del microcontrolador LPC1768, y finalmente dentro de ella he creado una nueva carpeta llamada Blueboard donde estará el proyecto de este post.

Por qué? Pues porque dentro de las carpetas Blueboard y MCB1700, existe un BSP específico para cada placa. La placa Blueboard no tiene la misma distribución que la placa MCB1700, por ello es necesario separar los proyectos con la placa Blueboard para no alterar el BSP del anterior. Por si no recuerdan BSP significa Board Support Package y se refiere a la distribución física de leds, lcd, teclado, buzzer, sensores, es decir todo lo que tenga tu placa.

Crear un task (tarea) para el uC/OS-III

En el libro está muy bien explicado como crear un task en este RTOS y para no caer en la absurda traducción de lo mismo haré un resumen de lo que deben saber:

  • Dentro del main() se debe crear tan sólo 1 (un) task al que se le puede llamar (no necesariamente) App_TaskStart.
  • Si tu aplicación va a tener más tasks, estas deben ser creadas en el interior de App_TaskStart, no en el main.
  • Cada task para existir requiere de lo siguiente: un task control block, un stack para sus datos, su prototipo y cuerpo donde va el código escrito por nosotros.
  • Un task es creado utilizando la función OSTaskCreate que contiene una serie de argumentos que definen el comportamiento de dicha tarea.

La gestión de las tareas y cómo funcionan será tema de un siguiente post, en este tan sólo haremos la creación y utilización. Si no aguantan la curiosidad pueden encontrar lo que deseen saber sobre los tasks en el Capítulo 5 – Task Managment del libro del uC/OS-III.

Los pines de entrada/salida GPIO del LPC1768

Ahora necesitamos el datasheet del LPC1768, para ello vamos a la siguiente dirección web: http://www.keil.com/dd/chip/4868.htm, y descargamos lo siguiente: User Manual. Dentro de ese pdf busquen Chapter 9: LPC17xx General Purpose Input/Output (GPIO). Pero antes de entrar en detalles del GPIO tenemos que conocer el patillaje del microcontrolador LPC1768:

Esta imagen la encontré aquí: ver enlace. Entonces, este micro CortexM3 posee 5 puertos GPIO: P0, P1, P2, P3 y P4, de los cuales cada puerto tiene la siguiente distribución:

En esta ocasión no usaremos todos los pines, tan sólo emplearé 1, bueno en realidad haremos ejemplos con 8 leds pero en este post sólo prenderemos uno de ellos. La distribución depende de la tarjeta que usen, en mi caso tengo que hacerlo para la Blueboard la cual tiene todos los pines disponibles pero seleccionaré los pines P1.20 hasta el P1.27 porque así me pareció XD. Entonces enfocaré el análisis del GPIO al puerto P1, pues si entienden bien el manejo del puerto P1 no tendrán dificultad para el resto de puertos.

Ahora sí vamos con los registros necesarios para configurar el puerto P1. Son 5 los registros que gobiernan los puertos GPIO del LPC1768. El FIODIR que se encarga de la dirección de los pines, si son salidas o entradas digitales, 1′s es para salidas y 0′s para entradas. El FIOMASK es un registro interesante, permite enmascarar la manipulación de las salidas y también afecta a las entradas de la siguiente forma: 0′s permiten cualquier acción de escritura o lectura en los bits del puerto, pero si los bits tienen 1′s entonces ignorará cualquier comando de lectura o escritura en dichos bits. El FIOPIN es para escribir directamente sobre el puerto cuyos bits sean salidas (0′s nivel bajo, 1′s nivel alto) y en el FIOMASK tengan 0′s, también permite leer el puerto cuyos bits sean entradas y en el FIOMASK tengan 0′s sino, se quedará con el valor anterior. El FIOSET con 1′s pone en nivel lógico alto los bits que sean salidas y que en el FIOMASK tengan 0′s. Mientras que el FIOCLR con 1′s  pone en nivel lógico bajo los bits que sean salidas y que en el FIOMASK tengan 0′s.

La asignación se hace bit a bit como en todos los micros que hemos visto. Observen que cada registro posee 32 bits (0×20) pero no necesariamente estan todos disponibles como el caso del puero P3 que sólo tiene dos GPIO.

Mapeo de los pines GPIO del puerto P1 en el uC/OS-III

El código para setear un pin del GPIO P1, en otras palabras, prender un led es el siguiente (no sé como rayos hacer para que no desaparezcan los tabs en wordpress, alguien sabe?):

static void App_TaskPrendeLed (void *p_arg)
{
OS_ERR err;
CPU_TS ts;

p_arg = p_arg;

while(1){

BSP_LED_On(8);

}

}

Estamos apreciando el task que se encarga de prender el led del que tanto hemos hablado. Pero dice BSP_LED_On(8), lo cual indica que el manejo está encapsulado en esa función que está en la carpeta BSP. Si indagamos y buscamos el origen de esta función encontraremos lo siguiente:

void  BSP_LED_On (CPU_INT08U  led)
{
switch (led) {
case 0u:
CSP_GPIO_BitSet(CSP_GPIO_PORT_NBR_01,
BSP_GPIO_PORT1_LED_GRP);
break;

case 1u:
CSP_GPIO_BitSet(CSP_GPIO_PORT_NBR_01,
BSP_GPIO_PORT1_LED_01);
break;

case 2u:
CSP_GPIO_BitSet(CSP_GPIO_PORT_NBR_01,
BSP_GPIO_PORT1_LED_02);
break;

case 3u:
CSP_GPIO_BitSet(CSP_GPIO_PORT_NBR_01,
BSP_GPIO_PORT1_LED_03);
break;

case 4u:
CSP_GPIO_BitSet(CSP_GPIO_PORT_NBR_01,
BSP_GPIO_PORT1_LED_04);
break;

case 5u:
CSP_GPIO_BitSet(CSP_GPIO_PORT_NBR_01,
BSP_GPIO_PORT1_LED_05);
break;

case 6u:
CSP_GPIO_BitSet(CSP_GPIO_PORT_NBR_01,
BSP_GPIO_PORT1_LED_06);
break;

case 7u:
CSP_GPIO_BitSet(CSP_GPIO_PORT_NBR_01,
BSP_GPIO_PORT1_LED_07);
break;

case 8u:
CSP_GPIO_BitSet(CSP_GPIO_PORT_NBR_01,
BSP_GPIO_PORT1_LED_08);
break;

default:
break;

}
}

Es un switch-case que evalúa el número que ingresemos y se encuentra en el archivo bsp.c dentro de la carpeta BSP. En nuestro ejemplo hemos ingresado el número 8 que selecciona la línea de código:

CSP_GPIO_BitSet(CSP_GPIO_PORT_NBR_01, BSP_GPIO_PORT1_LED_08);

Que a su vez recibe dos argumentos, el primero hace referencia al puerto 1 (su valor es 1 en decimal) y el otro al led8 (su valor es DEF_BIT_27 que a su vez es 0×08000000), y cómo sabemos eso? Pues ambos argumentos son etiquetas o labels que están definidos en el bsp.c y en el csp.h respectivamente, veamos:

Entonces si seguimos el rastro de  CSP_GPIO_BitSet encontraremos lo siguiente:

void SP_GPIO_BitSet (CSP_DEV_NBR port_nbr, CSP_GPIO_MSK pins)
{
CPU_SR_ALLOC();

#if (CSP_CFG_ARG_CHK_EN == DEF_ENABLED)
if (port_nbr > CSP_GPIO_PORT_NBR_04) {
return;
}
#endif

CPU_CRITICAL_ENTER();
CSP_GPIO_REG_FIOSETx(port_nbr) = pins;
CPU_CRITICAL_EXIT();
}

Este trozo de código se encuentra en el archivo csp_gpio.c, del cual nos interesa la línea de código que dice (del resto nos ocuparemos en otro post):

CSP_GPIO_REG_FIOSETx(port_nbr) = pins;

Y si buscamos la definición de CSP_GPIO_REG_FIOSETx encontraremos que es una macro para encapsular el registro FIOSET:

#define  CSP_GPIO_REG_FIOSETx(port_nbr)        (*(CPU_REG32 *)(CSP_GPIO_ADDR_FIO + ((port_nbr) * 32u) + 0x18))

De donde CSP_GPIO_ADDR_FIO es un label que está definido por:

Entonces si reemplazamos valores: CSP_GPIO_ADDR_FIO = 0x2009c000, port_nbr = 1, resultará:

CSP_GPIO_REG_FIOSETx = 0x2009 C038

Lo cual corresponde con la dirección  de FIO1SET, luego:

CSP_GPIO_REG_FIOSETx(port_nbr) = pins;   
FIO1SET = pins;   
FIO1SET = BSP_GPIO_PORT1_LED_08;
FIO1SET = 0x08000000

Con lo cual finalmente se activa tan sólo el pin P1.27, pero eso no es todo, y donde está la línea de código que configura a los pines P1.20 – P1.27 como salidas? Cómo estar seguros de que el FIO1MASK no está desactivando las escrituras en los pines de salida?

En el App_TaskStart existe una función llamada BSP_PostInit dentro de la cual están las configuraciones para el puerto P1 invocando a una función de encapsulamiento llamada CSP_GPIO_Cfg. Dicha función recibe una serie de argumentos de entrada que permiten configurar el puerto P1 con el label CSP_GPIO_PORT_NBR_01, y los pines para trabajar se seleccionan con el label BSP_GPIO_PORT1_LED_GRP, la dirección de los pines se realiza con el label CSP_GPIO_DIR_OUT. Con el label CSP_GPIO_FLAG_MODE_NONE le indicamos que los pines no tendrán pull-up ni pull-down. Luego con DEF_NO indicamos que no tienen interrupción. El argumento con 0 correspondía con la polaridad de la interrupción, como no tiene no interesa su valor. Finalmente con CSP_GPIO_FNCT_00 indicamos que los pines son gestionados por el GPIO Controller.

App_TaskStart y App_TaskPrendeLed

Ahora hablemos de los task creados en el proyecto PrendeLed. Como mencionamos líneas arriba el App_TaskStart es el primer task en ser creado (y el único dentro del main) y a partir de éste creamos el App_TaskPrendeLed.

Debemos recordar que cada tarea para ser creada requiere de un TaskControlBlock (App_TaskStartTCB), un Stack (App_TaskStartStk), sus respectivos Prototipo (App_TaskStart) y Cuerpo (el código de la App_TaskStart).

static  OS_TCB  App_TaskStartTCB;                       
static  CPU_STK App_TaskStartStk[APP_CFG_TASK_START_STK_SIZE];

static  OS_TCB  App_TaskPrendeLedTCB;                       
static  CPU_STK App_TaskPrendeLedStk[APP_CFG_TASK_START_STK_SIZE];

static  void  App_TaskStart         (void  *p_arg);
static  void  App_TaskPrendeLed     (void  *p_arg);

Este es el código del main donde es creado el App_TaskStart, fijense que se utiliza OSTaskCreate para crear tasks y requiere de muchos argumentos de entrada, estos argumentos serán explicados en el siguiente tutorial:

int  main (void)
{
OS_ERR   os_err;
BSP_PreInit();
CPU_Init();
OSInit(&os_err);

OSTaskCreate((OS_TCB      *)&App_TaskStartTCB,
(CPU_CHAR    *)"Start",
(OS_TASK_PTR  )App_TaskStart,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_START_PRIO,
(CPU_STK     *)&App_TaskStartStk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

OSStart(&os_err);
(void)&os_err;
return (0);

}

Recordar tambíen que el uC/OS-III es un RTOS preemptivo, es decir que siempre ejecutará la tarea más importante y para ello se ayuda de la prioridad asignada a cada tarea cuando es creada:

#define  APP_CFG_TASK_START_PRIO                           2u
#define  APP_CFG_TASK_PRENDELED_PRIO                       3u

Fíjense que estamos asignando a TaskStart una prioridad más alta que a TaskPrendeLed. Entonces el kernel, luego de ejecutar la primera tarea irá por la segunda y así sucesivamente de tal forma que cada tarea “pensará” que tiene el CPU dedicado para ella sola. Dentro de cada task o tarea, existe un while(1) donde se encuentra precisamente la esencia de la tarea a desarrollar por el kernel. Veamos que hace cada task.

El App_TaskStart, después de crear el TaskPrendeLed, lo único que hace en su bucle while(1) es ejecutar un delay de 100 milisegundos, y luego volverlo a ejecutar una y otra vez, and forever and ever….

static  void  App_TaskStart (void *p_arg)
{
OS_ERR       os_err;
(void)p_arg;

BSP_PostInit();
OS_CSP_TickInit();
OSTaskCreate((OS_TCB      *)&App_TaskPrendeLedTCB,
(CPU_CHAR    *)"PrendeLed",
(OS_TASK_PTR  )App_TaskPrendeLed,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_PRENDELED_PRIO,
(CPU_STK     *)&App_TaskPrendeLedStk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

while (DEF_TRUE) {
OSTimeDlyHMSM(0u, 0u, 0u, 100u,OS_OPT_TIME_HMSM_STRICT,&os_err);
}
}

Mientras que el App_TaskPrendeLed lo único que hace en su while(1) es mantener encendido el led8 por los siglos de los siglos…

static void App_TaskPrendeLed (void *p_arg)
{
OS_ERR err;
CPU_TS ts;

p_arg = p_arg;

while(1){
BSP_LED_On(8);
}

}

Veamos el siguiente video donde se observa el código en vivo y en directo:

Archivos:

Lo que estoy dejando para que descarguen es la carpeta Blueboard donde está la sub carpeta PrendeLed correspondiente a esta experiencia. Tomen como referencia la imagen para saber donde extraer la carpeta Blueboard.

4Shared

Boxnet

Henry Laredo Q.


Tutorial uC/OS-III y LPC1768 – Blinky Port Led

$
0
0

Este es el quinto tutorial dedicado al uC/OS-III vamos a continuar analizando el manejo de los tasks dentro del kernel para generar parpadeos (blinks) en los 8 leds disponibles en mi “trainer”, así mismo trataremos el tema de los retardos en el kernel para conocer más características de su entorno.

En el post anterior habiamos realizado una breve introducción al código, presentando lo necesario para mantener encendido un simple led mediante un task y nada más. Ahora vamos a implementar más tareas y analizar cómo interactúan entre ellas y cómo el kernel determina a quien le toca ejecutarse.

La distribución de los 8 leds están en los pines P1.20 hasta el P1.27 al igual que en el proyecto anterior los cuales están en el puerto P1.

Implementación de varias tareas en el Kernel

Como ya sabemos en el main del proyecto sólo se crea el App_TaskStart, dentro de dicho task recién podemos crear el resto de tasks de nuestra aplicación. En este post vamos a crear 5 tasks, de las cuales el App_TaskLed8 hace parpadear el led 8, el App_TaskLed7 hace parpadear el led 7, y así sucesivamente. El App_TaskLedGrupo sirve para implementar una corrediza con los leds 4, 3, 2 y 1.

static  void  App_TaskStart         (void  *p_arg);
static  void  App_TaskLed8          (void  *p_arg);
static  void  App_TaskLed7          (void  *p_arg);
static  void  App_TaskLed6          (void  *p_arg);
static  void  App_TaskLed5          (void  *p_arg);
static  void  App_TaskLedGrupo      (void  *p_arg);

No olviden que cada task por crear requiere de su propio task control block (TCB) y stack, aún no hemos hablado acerca del TCB ni del stack, y tampoco lo haremos en este post. Ya llegara su momento, por ahora asumamos tan solo su necesidad.

static  OS_TCB      App_TaskStartTCB;                               
static  CPU_STK     App_TaskStartStk[APP_CFG_TASK_START_STK_SIZE]; 

static  OS_TCB      App_TaskLed8TCB;                         
static  CPU_STK     App_TaskLed8Stk[APP_CFG_TASK_START_STK_SIZE];  

static  OS_TCB      App_TaskLed7TCB;          
static  CPU_STK     App_TaskLed7Stk[APP_CFG_TASK_START_STK_SIZE];     

static  OS_TCB      App_TaskLed6TCB;                  
static  CPU_STK     App_TaskLed6Stk[APP_CFG_TASK_START_STK_SIZE];  

static  OS_TCB      App_TaskLed5TCB;                 
static  CPU_STK     App_TaskLed5Stk[APP_CFG_TASK_START_STK_SIZE];

static  OS_TCB      App_TaskLedGrupoTCB;                 
static  CPU_STK     App_TaskLedGrupoStk[APP_CFG_TASK_START_STK_SIZE];

Hemos mencionado la palabra parpadear, lo que implica que va a estar prendido y apagado durante un perdiodo regular y contínuo, pero ¿cómo se consigue esto en el uC/OS-III? Veamos como está constituido el task App_TaskLed8:

static void App_TaskLed8 (void *p_arg) 
{
    OS_ERR os_err;
    p_arg = p_arg;

    while(1){        
        BSP_LED_Toggle(8);
        OSTimeDlyHMSM(0u, 0u, 0u, 100u, OS_OPT_TIME_HMSM_STRICT, &os_err);
    }
}

Observar que aparecen dos funciones nuevas: BSP_LED_Toggle y OSTimeDlyHMSM. En el anterior proyecto utilizamos el BSP_LED_On, y ahora aparece la función BSP_LED_Toggle la cual permite hacer un cambio de estado en un pin de salida. Con esto se consigue modificar el nivel lógico anterior de un pin de salida sin afectar los demás pines. El periodo del parpadeo lo determina la función OSTimeDlyHMSM, la cual vamos explicar en detalle más adelante en este post, por ahora tan sólo diremos que el 100u que observan en uno de sus argumentos de entrada corresponde con las unidades de los milisegundos del retardo, por lo tanto estamos especificando un periodo de 100 milisegundos para el parpadeo del led 8.

El resto de tasks tienen la misma estructura sólo que se diferencian por el led que “togglean” y el periodo del “toggleo” o parpadeo.

static void App_TaskLed8 (void *p_arg) {
OS_ERR os_err;
p_arg = p_arg;
while(1){
BSP_LED_Toggle(8);
OSTimeDlyHMSM(0u, 0u, 0u, 100u, OS_OPT_TIME_HMSM_STRICT, &os_err);
}
}

static void App_TaskLed7 (void *p_arg) {
OS_ERR os_err;
p_arg = p_arg;
while(1){
BSP_LED_Toggle(7);
OSTimeDlyHMSM(0u, 0u, 0u, 250u, OS_OPT_TIME_HMSM_STRICT, &os_err);
}
}

static void App_TaskLed6 (void *p_arg) {
OS_ERR os_err;
p_arg = p_arg;
while(1){
BSP_LED_Toggle(6);
OSTimeDlyHMSM(0u, 0u, 0u, 325u, OS_OPT_TIME_HMSM_STRICT, &os_err);
}
}

static void App_TaskLed5 (void *p_arg) {
OS_ERR os_err;
p_arg = p_arg;
while(1){
BSP_LED_Toggle(5);
OSTimeDlyHMSM(0u, 0u, 0u, 480u, OS_OPT_TIME_HMSM_STRICT, &os_err);
}
}

Como pueden apreciar hasta ahí no hay nada fuera de lo común. El otro task implementado se llama App_TaskLedGrupo y su “tarea” es la de realizar un corredizo en los Led 4 3 2 1, utilizando una función hecha por nosotros llamada BSP_LED_Play, y lo único que hace esta función es prender el Led1 si la variable local “estado” es igual a 1, luego hay un delay de 650 milisegundos, a continuación activa el Led2 y así  hasta el Led4, luego reinicia en el Led1 y repite denuevo la secuencia.

static void App_TaskLedGrupo (void *p_arg) {
OS_ERR os_err;
CPU_INT08U estado = 1;
p_arg = p_arg;
while(1){
BSP_LED_Play(estado++);
if (estado >= 5) {
estado = 1;
}
OSTimeDlyHMSM(0u, 0u, 0u, 650u, OS_OPT_TIME_HMSM_STRICT, &os_err);
}
}

Y cómo hace el uC/OS-III para saber cual de los tasks ejecutar primero? Luego cómo sabe cual continúa? Recordemos que el uC/OS-III es un kernel preemptivo, es decir que siempre estará ejecutando la tarea más importante en el momento, siempre apoyándose en las prioridades asignadas a cada tarea al ser creadas.

Veamos el código del App_TaskStart, pues aquí es donde creamos las 5 tareas del proyecto. Observen el argumento de tipo OS_PRIO presente en la función OSTaskCreate:

static  void  App_TaskStart (void *p_arg)
{
OS_ERR       os_err;
(void)p_arg;

BSP_PostInit();
OS_CSP_TickInit();

OSTaskCreate((OS_TCB      *)&App_TaskLed8TCB,
(CPU_CHAR    *)"Led8",
(OS_TASK_PTR  )App_TaskLed8,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_LED8_PRIO,
(CPU_STK     *)&App_TaskLed8Stk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

OSTaskCreate((OS_TCB      *)&App_TaskLed7TCB,
(CPU_CHAR    *)"Led7",
(OS_TASK_PTR  )App_TaskLed7,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_LED7_PRIO,
(CPU_STK     *)&App_TaskLed7Stk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

OSTaskCreate((OS_TCB      *)&App_TaskLed6TCB,
(CPU_CHAR    *)"Led6",
(OS_TASK_PTR  )App_TaskLed6,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_LED6_PRIO,
(CPU_STK     *)&App_TaskLed6Stk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

OSTaskCreate((OS_TCB      *)&App_TaskLed5TCB,
(CPU_CHAR    *)"Led5",
(OS_TASK_PTR  )App_TaskLed5,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_LED5_PRIO,
(CPU_STK     *)&App_TaskLed5Stk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

OSTaskCreate((OS_TCB      *)&App_TaskLedGrupoTCB,
(CPU_CHAR    *)"LedGrupo",
(OS_TASK_PTR  )App_TaskLedGrupo,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_LEDGRUPO_PRIO,
(CPU_STK     *)&App_TaskLedGrupoStk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

while (DEF_TRUE) {
OSTimeDlyHMSM(0u, 0u, 0u, 100u,
OS_OPT_TIME_HMSM_STRICT,
&os_err);
}
}

Observen que las distintas prioridades estan dadas por unos labels que han sido previamente definidos en el archivo app_cfg.h y tienen los siguientes valores:

#define  APP_CFG_TASK_START_PRIO                        2u
#define  APP_CFG_TASK_LED8_PRIO                         3u
#define  APP_CFG_TASK_LED7_PRIO                         4u
#define  APP_CFG_TASK_LED6_PRIO                         5u
#define  APP_CFG_TASK_LED5_PRIO                         6u
#define  APP_CFG_TASK_LEDGRUPO_PRIO                     7u

Cada task tiene una prioridad distinta. Mientras menor es el número mayor es la prioridad. Si bien es cierto que el uC/OS-III permite tener varios tasks con la misma prioridad, por ahora evitaremos esos contextos pues corresponden con el algoritmo Round Robin y mejor será explicarlo más adelante cuando tengamos más experiencia con el kernel.

Por ahora nos compete entender cómo hace el uC/OS-III para determinar la ejecución de estas tareas. En este kernel es necesario que los tasks estén esperando por un evento, en este caso esperan la expiración de un retardo o delay, y mientras un taks espera su delay el kernel mira a la siguiente tarea por orden de prioridad y la ejecuta. Si hacemos un diagrama básico de lo que estamos desarrollando quedaría así:

Analizemos el gráfico expuesto. A la izquierda en orden de prioridad estan los tasks y sobre todas ellas está el kernel del uC/OS-III. Entonces en (1) el kernel se entera de la creación de TaskLed8 y como su prioridad es la mayor en ese momento, procede a ejecutar el task el cual contiene un evento de espera (OSTimeDlyHMSM) de 100 milisegundos. Dicho evento permite al kernel quitarle el CPU a TaskLed8 y continuar buscando la siguiente tarea en importancia mientras corre el delay del TaskLed8. Es cuando en (2) el kernel se entera de la creación de TaskLed7 la cual también tiene un evento de espera de 250 milisegundos y procede de la misma forma que con el anterior task. Y así con todos los tasks del gráfico, ya que todos tienen el mismo evento de espera pero distintos niveles de prioridad (importancia para el kernel).

Para cuando el kernel llega al estado (6) ha terminado de crear y ejectuar los 5 tasks. En adelante lo que hace es seguir ejecutando otros tasks internos propios del kernel (este tema será expuesto en otro post). Cuando pasan 100 milisegundos despues de ejecutar TaskLed8 por primera vez (puesto que expiró su delay), el kernel vuelve a atender dicho task para ejecutarlo otra vez como se puede apreciar en el estado (7). En el estado (8) vuelve a ejectuar el TaskLed8 por tercera vez y 50 milisegundos después ejecuta el TaskLed7 ya que su tiempo de espera era de 250 milisegundos.

Obviamente el gráfico no está a una escala correctamente proporcional, las creaciones de los tasks se producen en microsegundos! Pero aparecen nuevas interrogantes, por ejemplo, ¿cómo se da cuenta el kernel que pasaron exactamente 100 milisegundos? ¿el kernel puede contar en milisegundos o en microsegundos? ¿el kernel usa un timer por hardware del micro para hacer los conteos? ¿en qué momento se habilitó ese timer?

El SysTick  o System Tick Timer

El timer por hardware que permite generar interrupciones en periodos configurables se llama SysTick, está presente en todos los microcontroladores con arquitectura Cortex-M3 y por ello en el LPC1768 figura un módulo llamado System Tick Timer. Es un timer de 24 bits cuyo propósito principal es de proporcionar al sistema un mecanismo de control primario de temporización y es muy usado en la implementación de RTOS como en el caso del uC/OS-III.

Este módulo en el LPC1768 está conformado por 4 registros, donde el STCTRL se encarga de configurar y dar status del timer, el STRELOAD es el registro que lleva la cantidad de ticks (flancos de reloj del CPU) que debe contar el timer para recién generar un evento o interrupción, el STCURR es el registro que permite conocer el valor actual del timer, y el STCALIB es un registro que tiene preprogramado un valor para generar una interrupción cada 10 milisegundos cuando la frecuencia del CPU es 100MHz. El timer al ser habilitado desde STCTRL, carga la cantidad de ticks que contará desde STRELOAD y se desarrolla de forma decremental. Cuando llega a cero recarga su valor desde STRELOAD e informa mediante un flag en el registro STCTRL el cual se encarga luego de generar interrupción.

En nuestro caso estamos corriendo el CPU a 100MHz, entonces cada tick tiene (1/100MHz) segundos, podemos contar desde 1 tick hasta 16777215 (0xFF FF FF), lo que resultaría en un mínimo de 0.01 useg y un máximo de 0.16 segundos.

Este timer es precisamente usado por el uC/OS-III para realizar esos eventos de espera como el OSTimeDlyHMSM, veamos a continuación cómo sucede esto.

La tarea interna Tick Task y el Tick Interrupt

Es hora de ponerse serio, porque vamos a hablar del task interno Tick Task y de la interrupción que lo origina llamada Tick Interrupt. En el uC/OS-III tenemos encapsuladas todas las funcionalidades del LPC1768, es decir que pertenecen a capas inferiores de tal manera que el programador no puede acceder a ellas directamente, sino mediante funciones implementadas, el principal  motivo de esto es permitir la portabilidad del código del uC/OS-III en otras arquitecturas. Por ello el manejo del SysTick está en capsulado en el TickStack. En la siguiente imagen pueden observar una síntesis del funcionamiento del Tick Task y del Tick Interrupt y de algunos otros bloques que participan.

Todo esto junto con el kernel permiten la espera de retardos dentro de otros tasks y así distribuir los recursos del CPU entre las tareas. A continuación describiremos todos los bloques.

El primer bloque que tenemos que reconocer es el que tiene la función OS_CSP_TickInit().

Dentro del código del App_TaskStart, cuando es ejecutado por primera vez, y antes de su bucle while(1), se invoca a la función OS_CSP_TickInit().

static  void  App_TaskStart (void *p_arg)
{
    OS_ERR       os_err;   
    (void)p_arg;

    BSP_PostInit();       /* Initialize BSP functions */    
    OS_CSP_TickInit();    /* Initialize the Tick interrupt */

    OSTaskCreate((OS_TCB      *)&App_TaskLed8TCB,  
                 (CPU_CHAR    *)"Led8",
                 (OS_TASK_PTR  )App_TaskLed8, 
                 (void        *)0,
                 (OS_PRIO      )APP_CFG_TASK_LED8_PRIO,
                 (CPU_STK     *)&App_TaskLed8Stk[0],
                 (CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
                 (CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,

Dentro del código de la función OS_CSP_TickInit() ubicaremos la inicialización del Systim Tick Timer así como el calculo de los ticks que tiene que contar, configurado mediante el label OSCfg_TickRate_Hz. Recién en la función OS_CPU_SysTickInit se encuentra la manipulación directa de los registros del System Tick Timer correspondientes al LPC1768.

void  OS_CSP_TickInit (void)
{
    CPU_INT32U  cnts;
    CPU_INT32U  cpu_freq;

    cpu_freq = CSP_PM_CPU_ClkFreqGet();  
    cnts     = (cpu_freq / OSCfg_TickRate_Hz); 

    OS_CPU_SysTickInit(cnts); 
}

El label OSCfg_Tick_Rate_Hz se encuentra encapsulada a través de otro label, se trata de OS_CFG_TICK_RATE_HZ el se ubica en el archivo os_cfg_app.h, y está configurado por defecto para que el SysTick genere intervalos de 1 milisegundo.

Observen el valor de OS_CFG_TICK_RATE_HZ está en 1000u, lo que significa que generará una interrupción cada 1000Hz o cada 1 milisegundo. El valor 1000 no es cargado directamente en el registro STRELOAD, sino que recibe un tratamiento interno para que resulte en el valor final de 99999 (dentro de OS_CSP_TickInit hace la división FoscCPU/OS_CFG_TICK_RATE_HZ = 100MHz/1000Hz, luego a ese reasultado se le resta – 1 dentro de OS_CPU_SysTickInit). Lo cual recién nos indica la cantidad de ticks que contará el System Tick Timer.

Debido a que el LPC1768 está configurado a 100MHz (la explicación del sistema de relojeo del LPC1768 se verá en otro post) entonces si realizan el siguiente calculo: tiks/FoscCPU = 99999/(100MHz) =  0.99999 ~ 1 milisegundos. Observar que el kernel indica que no puedes generar intervalos menores de 10Hz y mayores de 1000Hz, porque?, si bien es cierto nosotros podemos cargarle a STRELOAD cualquier valor (dentro de sus 24 bits) la cuestión salta cuando le apliquemos por decir un intervlo de 100 microsegundos, esto generaría muchas llamadas constantes a la interrupción y por ende los 100Mhz del CPU no serían suficientes para implementar un kernel de tiempo real. En el caso de poner un valor inferior a 10Hz es porque no es necesario tener intervalos tan largos, si quieres tiempos largos para eso existen la funcion OSTimeDlyHMSM.

Hasta ahí tan solo hemos configurado el SysTick Timer para generar los intervalos contínuos de 1 milisegundo, pero la historia no termina ahí.

Internamente el kernel al ser inicializado en la función main, crea otras tareas internas, entre las cuales está la llamada OS_TickTask(), que es precisamente la que se encarga de gestionar todas las demás tareas que estén esperando la expiración de tiempos de espera, es decir a todos los tasks que han invocado a la función OSTimeDlyHMSM. Pero antes de que aparezca a tallar el OS_TickTask está la interrupción que la invoca.

Como recordaran el System Tick Timer genera una interrupción, esta interrupción en el uC/OS-III se llama OS_TimeTick, y en libro del uC/OS-III en la pagina 181, encontrarán THE CLOCK TICK en donde se explica my bien y con detalle que sucede dentro del ISR (interrupt service rutine) del SysTick, para que voy a traducir! Mencionaré que el Tick Interrupt se encarga de llamar al task del SysTick, es decir a OS_TickTask.

En la pagina 117 del libro del uC/OS-III está una muy detallada explicación del Tick Task (OS_TickTask). No creo conveniente extenderme en su funcionamiento interno ya que su uso pertenece exclusivamente al kernel, nosotros como programadores no tenemos acceso a esta función de forma directa. Pero si quieren averiguar los detalles pueden leer la sección indicada del libro.

La función OSTimeDlyHMSM()

El capítulo 11 Time Management del libro del uC/OS-III (no confundir con el capítulo 12 que se llama Timer Managment) está dedicado exclusivamente a las funciones que permiten esperar tiempos, como el OSTimeDlyHMSM.

Esta función permite a una tarea retardarse por un tiempo específico definido en horas, minutos, segundos y milisegundos. En la pagina 567 del mismo libro está la definición de los argumentos de la función OSTimeDlyHMSM el cual tiene la siguiente estructura:

Los campos que corresponden con hours, minutes, seconds y milli, se sobreentienden de que tratan cierto? Lo que se debe mencionar es que los rangos de valores que aceptan dependen de el modo de carga deseado para esta función, el cual es determinado por opt y pueden tomar los siguientes valores:

Con todo este background ya podemos entender cómo es que el kernel genera los parapedeos simultaneos y cómo la segmentación por tareas nos ayuda mucho sobretodo porque ya no tenemos que preocuparnos de la sincronización, tan sólo tienes que pensar en el periodo del blinkeo y que pines de salida lo reflejarán.

Veamos el siguiente video donde se muestra el código del proyecto con la implementación en la Blueboard:

Archivos:

En esta oportunidad he subido sólo la carpeta BSP y la carpeta del proyecto, descompriman todo dentro de la carpeta Blueboard\KeilMDK porque hice unos cambios en el BSP y necesitan chancar el anterior BSP, no se preocupen que ese nuevo BSP no afecta en el proyecto anterior PrendeLed.

4shared

Boxnet

Henry Laredo Q.


Tutorial uC/OS-III y LPC1768 – Elementos de un Task

$
0
0

En este tutorial vamos a extender el análisis de los Tasks para conocer los detalles de su naturaleza en el kernel. Vamos a hablar sobre los elementos de un Task que son: el task control block, el stack, prioridades y parámetros inicializados en la función OSTaskCreate.

Elementos de un Task

El Capítulo 5 – Task Management – del libro del uC/OS-III, está dedicado enteramente al funcionamiento y configuración de un Task. Nos basaremos en esta lectura para realizar el presente documento. Como siempre evitaré caer en la mera traducción, ya que la información de Micrium es muy precisa y exacta, pero requiere de cierto background para entenderla, y el propósito nuestro es precisamente ése, borrar limitantes.

Vamos al asunto. Todo Task requiere de un loop infinito (el while(1){…}) y a su vez dicho loop deberá de invocar a un servicio o función del kernel (como por ejemplo el OSTimeDlyHMSM) lo que causará que el Task se quede esperando a que ocurra cierto evento (provocado por el servicio invocado).

Debemos tener siempre presente que es sumamente importante que cada tarea de nuestra aplicación espere por un evento, pues de lo contrario el Task se convertirá en un verdadero bucle infinito y estará jalando todo el consumo del CPU evitando que el kernel atienda otras tareas de la aplicación.

Precisamente, cuando un Task está esperando por un evento, éste no consume recursos del CPU, es ahí donde el kernel puede comenzar a atender diversas tareas dando pie al concepto de multitarea o multitasking.

Para que todo esto ocurra, el kernel debe estar al tanto de todas los Tasks de la aplicación para que sean gestionadas por él, y para ello cada Task requiere tener asignado los siguientes elementos:

  • La Prioridad.
  • Un Stack.
  • Los Parámetros (cargados mediante la función OSTaskCreate).
  • Un Task Control Block (en adelante TCB).

Veamos a continuación cada uno y tratemos de establecer la relación que existe entre ellos.

La Prioridad de un Task

La Prioridad es un número que le ayuda al kernel en la determinación de la siguiente tarea más importante en ejecutar (comportamiento de un kernel preemptivo). Cada Task tiene asignado un número que corresponde con su prioridad. Este número al ser mucho menor implica mayor importancia y viceversa.

Supuestamente la máxima prioridad que podemos utilizar para nuestros Tasks es 1 y la mínima viene dada por el valor OS_CFG_PRIO_MAX-2, los dos valores inferiores (OS_CFG_PRIO_MAX-1 y OS_CFG_PRIO_MAX) que se prohiben usar están destinados para los tasks internos del kernel (como el tick task). En nuestro proyecto viene por defecto el valor OS_CFG_PRIO_MAX = 18 (se encuentra en el archivo os_cfg.h), es decir que podemos asignar hasta 18 niveles de prioridad, sin embargo es posible poner cualquier valor superior ya que sólo depende de la cantidad de memoria RAM disponible en nuestro CPU (microcontrolador LPC1768).

Sólo para conocimiento, debo mencionar que los tasks no necesariamente deben tener prioridades distintas, pueden compartir la misma prioridad. Sin embargo al hacer esto estaremos forzando al kernel a emplear el algoritmo de Round Robin Scheduling. El cual aún no hemos tratado pero que ya será motivo de otro post. Por ahora asignemos siempre distintas prioridades a nuestros tasks.

El Stack de un Task

El Stack es una porción de memoria (alojado en la RAM del CPU) que contiene datos muy relevantes para el Task al que le pertenece. Por ello, todo Task de forma obligatoria requiere de un Stack. Así mismo, el kernel al ser preemptivo se apoya en el Stack para tener un registro o seguimiento de las variables locales, llamadas a funciones y anidación de interrupciones (ISR nesting), que se generen en el código del Task.

El tamaño del Stack es declarado cuando se instancian las variables estáticas de un Task en el main.c, veamos por ejemplo que en nuestro proyecto a todos los Stacks les asignamos el mismo tamaño y que viene referido por el label APP_CFG_TASK_START_STK_SIZE, el cual es igual a 128 (archivo app_cfg.h). Pero no asuman que ese número refleja unidades de bytes. Fíjense en la variable estática App_TaskLed8Stk, observen es un array de 128 elementos donde el tipo de dato viene dado por CPU_STK.

El tipo de dato CPU_STK se refiere al tamaño de datos en el CPU y en nuestro caso que usamos un CortexM3, el tamaño de datos corresponde con 32 bits. Por ello 128 de Stack sería 4 x 128 Bytes = 512 Bytes para el Stack de un Task en nuestra aplicación. El cálculo de cuanto tamaño debe tener el Stack en cada Task es aún de forma manual, puedes asignarle un valor grande y luego ir analizando en la aplicación si usa todo el espacio dedicado para ir cortandolo, es como el ensayo-y-error, ya trataremos este tema cuando implementemos Task más complejos.

Como mencionamos, en el Stack se guardan las variables locales, llamadas a funciones y anidación de interrupciones (ISR nesting) utilizadas en el Task al que pertenece. Ya conocemos que para nuestro proyecto la cantidad de espacios para el Task es 128 (stk_size) y el tamaño de estos espacios es de 32 bits (CPU_STK), luego el elemento [0] del array del Stack se le conoce como p_stk_base y es la posición más baja del Stack. En las posiciones altas se guardan los registros del CPU que son utilizados para hacer intercambio de contextos (Context Switching).

Mucha información presente en el Stack se complementa con los otros elementos del Task que veremos a continuación (TCB y Parámetros).

Los Parámetros de un Task (OSTaskCreate)

En la página 501 del libro del uC/OS-III está la descripción completa de todos los parámetros de un Task que son inicializados por la función OSTaskCreate.

Para no estar traduciendo sin aportar nada nuevo, haremos una tabla que sintetiza el texto del libro:

Ahora veamos la forma en que hemos estado creando nuestros tasks mediante el OSTaskCreate, para que tengan una idea de cómo es la forma de ingresar los parámetros en esta función:

OSTaskCreate((OS_TCB      *)&App_TaskLed8TCB,
(CPU_CHAR    *)"Led8",
(OS_TASK_PTR  )App_TaskLed8,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_LED8_PRIO,
(CPU_STK     *)&App_TaskLed8Stk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

OSTaskCreate((OS_TCB      *)&App_TaskLed7TCB,
(CPU_CHAR    *)"Led7",
(OS_TASK_PTR  )App_TaskLed7,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_LED7_PRIO,
(CPU_STK     *)&App_TaskLed7Stk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

OSTaskCreate((OS_TCB      *)&App_TaskLed6TCB,
(CPU_CHAR    *)"Led6",
(OS_TASK_PTR  )App_TaskLed6,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_LED6_PRIO,
(CPU_STK     *)&App_TaskLed6Stk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

OSTaskCreate((OS_TCB      *)&App_TaskLed5TCB,
(CPU_CHAR    *)"Led5",
(OS_TASK_PTR  )App_TaskLed5,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_LED5_PRIO,
(CPU_STK     *)&App_TaskLed5Stk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

OSTaskCreate((OS_TCB      *)&App_TaskLedGrupoTCB,
(CPU_CHAR    *)"LedGrupo",
(OS_TASK_PTR  )App_TaskLedGrupo,
(void        *)0,
(OS_PRIO      )APP_CFG_TASK_LEDGRUPO_PRIO,
(CPU_STK     *)&App_TaskLedGrupoStk[0],
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE_LIMIT,
(CPU_STK_SIZE )APP_CFG_TASK_START_STK_SIZE,
(OS_MSG_QTY   )0u,
(OS_TICK      )0u,
(void        *)0,
(OS_OPT       )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR      *)&os_err);

}

Como pueden ver hay parámetros que estamos dejándolos en 0 (cero) los cuales por ahora no estamos utilizando y por ende reservaré su estudio para posteriores entregas.

Sobre las opciones de configuración del Task (OS_OPT opt) los parámetros son los siguientes:

Si requieren profundizar en este tema de los parámetros pueden consultar siempre el libro del uC/OS-III.

El Task Control Block (TCB)

Es una estructura de control utilizada por el kernel e implementada en RAM que permite conocer información relevante acerca de un Task. Es importante conocer y entender cada campo de esta estructura sin embargo el código de aplicación NUNCA tendrá acceso directo a estos campos (sobretodo modificarlos). Por ello el TCB debe ser administrado siempre por el kernel. El TCB es creado en el main.c como una variable estática donde el tipo es OS_TCB, la cual está previamente definida en el uC/OS-III.

Vamos a ver los campos que constituyen la estructura del TCB y agruparlos en función a los objetivos que cumplen:

Son un total de 51 campos que se inicializan a través de la función OSTaskCreate, en el momento de crear un nuevo Task. Finalmente revisen la página 105 del libro del uC/OS-III donde se brinda la información completa y exacta de cada campo del TCB. Mencionar algo acá sería una mera traducción de lo mismo, por ello para una mejor comprensión me pareció adecuado agruparlos y complementarlo junto con la lectura del libro. Presten atención sobre todo a los bloques Task Management, Time Management y Stack, pues son los campos que más hemos utilizado hasta el momento.

Sintetizando lo aprendido

Bueno estimados lectores, si han llegado hasta aquí es porque realmente les interesa dejar a un lado la antigua forma de programación micros para abrazar el mundo de los RTOS. Una buena forma de ver lo que hemos aprendido hasta el momento es revisar el esquema de aprendizaje que presenta el libro uC/OS-III sobre el cual nos basamos. Los bloques que ven con un check color rojo son aquellos que hemos estudiado en el blog.

Ahora ya sabemos cómo crear un nuevo Task, también cómo debe funcionar acorde con la naturaleza de un kernel preemptivo como el uC/OS-III, así mismo hemos visto cómo utilizar un evento de expiración de tiempo.

Lo que sigue será explorar otros bloques del esquema mostrado para seguir conociendo las capacidades y fortalezas de un RTOS.

Henry Laredo Q.


Tutorial uC/OS-III y LPC1768 – LCD2x16 con Transferencia de Mensajes

$
0
0

En este tutorial vamos a explorar la trasnferencia de mensajes entre Tasks (Message Passing) haciendo uso de los Task Message Queue propios de cada Task, todo esto dentro del uC/OS-III. Para ello se implementará un proyecto donde se hara uso de una pantalla LCD2x16 para mostrar un mensaje corredizo que será acompañado del resto de Tasks que hemos implementado hasta la fecha.

Como recordarán un Task es una función con un bucle infinito y que cree tener la atención exclusiva del CPU con todos sus recursos. Además que un Task no puede retornar como si fuese cualquier función en C. Entonces cómo podríamos hacer para que un Task le pase información a otro Task?

El capítulo 15 del libro del uC/OS-III nos describe los Messages Passing o Transferencia de Mensajes que están implementados en el kernel para precisamente intercambiar información entre los Tasks. Por ejemplo, tenemos un Task que gestiona el arranque de la LCD2x16, otro Task que gestiona la posición y escritura de cada caracter, y otro que gestiona el desplazamiento de un mensaje predefinido para dar el efecto corredizo. Es obvio que entre estos tres tasks hay información que tiene que ser intercambiada. El task que gestiona el arranque debe tener la mayor prioridad de los tres, el que gestiona la escritura de datos es el siguiente en importancia y finalmente el task que gestiona el desplazamiento enviará información al task que gestiona la escritura de caracteres.

Además necesitamos implementar algo nuevo, la eliminación de tareas, ya que la primera tarea no puede existir despues de haber cumplido su misión. Sino estaría re-iniciando el LCD2x16 siempre. Por ello antes de ir con los Messages Passing vamos a darle una ojeada a lso Task States, un tema que ahora sí toma relevancia conocer.

Task States – Punto de Vista Usuario

Si no hemos tratado este antes fue porque no era tan importante saber de ello antes, ya que solo implementabamos tasks que no tenian nada que ver uno con el otro. Ahora es distinto. Tendremos un task que debe ejectuarse para luego eliminarse, y otros que tienen que enviar información y estar pendientes de la recepción del mensaje.

En el capítulo 5 página 100 del uC/OS-III se encuentra el tema Task States, sugiero su lectura pues explica con mucho detalle y ejemplos en qué consisten estos estados, presenta dos puntos de vista: usuario y kernel. De nuestro lado en el blog, basta con mencionar el punto de vista de usuario.

Todo Task requiere de ciertos elementos para ser implementado, la existencia de estos elementos que conforman un Task permiten establecer el estado Dormant (1) del Task que significa algo así como inactivo porque el kernel no está enterado de su existencia para añadirlo en su gestión. Luego cuando en el código main.c decidimos crear el Task utilizando OSTaskCreate, recién ahí le informamos al kernel de la existencia de dicho Task, y este estado se conoce como Ready (2) que significa listo para ser ejecutado por el kernel (según su nivel de prioridad). Si en un momento dado el kernel decide que este Task es el más importante entonces lo ejecuta y ese estado se conoce como Running (3). Luego imaginen que está corriendo el TickTask y se dispara su interrupción, entonces aquel ISR va a interrumpir el proceso de ejecución de nuestro Task (siempre y cuando siga en el estado Running (3) ), y se quedará en el estado Interrupted (5) hasta que el kernel retorne su atención despues de procesar la interrupción. Dentro del bucle infinito del Task siempre está esperando por un evento, como el OSTimeDlyHMSM, cuando hace esto está en el estado Pending (4) para luego retornar al estado Ready (2) y esperar denuevo a que el kernel designe si es la tarea con mayor importancia para ejectuarla y pasar al estado Running (3). Así mismo podemos hacer que un Task despues de estar ejecutándose en el modo Running (3) sea eliminada mediante OSTaskDel y pase al estado Dormant (1), esto realmente no borra ninguna línea de código, tan solo retira a nuestro Task de la lista de gestión del kernel.

Ven que fácil fue entenderlo?

Message Passing – Transferencia de Mensajes

Hay dos formas de hacer las transferencias de mesanjes, empleando Message Queues (cuando tienes multiples tasks esperando por mensajes de un mismo message queue) o utilizando Task Message Queues (cuando se conoce con exactitud que task va a procesar el mensaje enviado). Para el propósito de este post sólo explicaremos la implementación de Task Message Queues.

Qué es un Message Queue? Viene a significar cola de mensajes y dentro del kernel es tratado como un objeto externo. Y qué es entonces un Task Message Queue? Dice el libro que cada Task tiene implementado en su TCB (Task Control Block) un message queue para que enviemos mensajes directamente a dicho Task para evitar el uso de objetos externos.

Funciona así, un Task o un ISR pueden enviar un mensaje (mediante los servicios del uC/OS-III que veremos luego) a un Task específico hacia la cola de mensajes de éste ultimo. Para esto el Task que recepciona el mensaje debe estar en estado de espera (Pending) para que pueda leer el mensaje enviado. Y el evento que permite que espere un mensaje de su message queue se realiza con el servicio OSTaskQPend().

Además de esta forma básica de enviar y recibir se pueden implentar variantes para añadir seguridad y coherencia a la transmisión de mensajes entre tasks, ya que pueden existir casos donde un Task esté enviando mensajes más rápido de lo que el Taks receptor pueda procesarlos y como consecuencia puede haber pérdida de información.

Tenemos un método que se conoce como Bilateral Rendezvous en el cual se sincronizan las actividades de dos Tasks con la limitancia de que cada Task Message Queue sólo puede alojar un mensaje.

Y tambien tenemos un control de flujo combinando los Task Message Queue y los Task Semaphores (este tema quedará pendiente para el siguiente post), donde el Task que envia el mensaje es controlado por el receptor para enviar un siguiente contenido. Vamos a utilizar este método en nuestro proyecto al tener no tener la limitancia del anterior.

Los Servicios del Task Message Queue

Ya hemos mencionado escuetamente algunos de los servicios, ahora veamos un poco en detalle todos los servicios que proporciona el kernel para los Task Message Queue:

OSTaskQFlush().- Permite vaciar el contenido del message queue interno de un task, tiene los siguientes argumentos:

  • p_tcb: Es un puntero al OS_TCB del task.  Especificando un puntero NULL indica que se desea vaciar el message queue del task que invoca la función.
  • p_err: Es un puntero a una variable que va a recibir un código de error retornado por esta función.

OS_ERR_NONE                 sin errores
OS_ERR_FLUSH_ISR      si se invoco esta función desde un ISR

  • Retorna: El número de entradas liberadas en el queue.

OSTaskQPend().- Permite que el presente task espere por un mensaje que le sea posteado, tiene los siguientes argumentos:

  • timeout: Es un timeout opcional en unidades de clock ticks.  El task esperará por la llegada de un mensaje hasta la cantidad de tiempo especificada. Si la cantidad es 0 (cero) entonces el task esperará por siempre hasta que llegue un mensaje.
  • opt: Determina si  se suspende el presente task si el queue está vacío o no.

OS_OPT_PEND_BLOCKING
OS_OPT_PEND_NON_BLOCKING

  • p_msg_size: Es un puntero a una variable que recibirá el tamaño del mensaje.
  • p_ts: Es un puntero a una variable que recibirá el timestamp de cuando el mensaje fue posteado.
  • p_err: Es un puntero a una variable que va a recibir un código de error retornado por esta función.

OS_ERR_NONE                                   sin errores
OS_ERR_PEND_ABORT                    si la espera fue cancelada
OS_ERR_PEND_ISR                           si se invoco esta función desde un ISR
OS_ERR_PEND_WOULD_BLOCK   si se usa non-blocking y no hay mensajes.
OS_ERR_Q_EMPTY
OS_ERR_SCHED_LOCKED               si el scheduler está bloqueado.
OS_ERR_TIMEOUT                           si el mensaje no llego dentro del timeout.

  • Retorna: Un puntero al mensaje recibido.

OSTaskQPendAbort().-  Permite cancelar la espera por un mensaje y pone en estado Ready al task especificado, tiene los siguientes argumentos:

  • p_tcb: Es un puntero al task cuya espera será cancelada.
  • opt: Provee opciones para esta función:

OS_OPT_POST_NONE               Sin opción específica
OS_OPT_POST_NO_SCHED     Indica que el scheduler no será llamado.

  • p_err:  Es un puntero a una variable que va a recibir un código de error retornado por esta función.

OS_ERR_NONE                                 sin errores
OS_ERR_PEND_ABORT_ISR        si se invoco esta función desde un ISR
OS_ERR_PEND_ABORT_NONE   si el task no estaba esperando mensajes
OS_ERR_PEND_ABORT_SELF    si el puntero es tipo NULL en ‘p_tcb’

  • Retorna:

DEF_FALSE   si el task no estaba esperando mensajes o sucedió un error
DEF_TRUE    si el task fue preparado para ejecutarse por esta función

OSTaskQPost().- Permite enviar un mensaje a un determinado task, tiene los siguientes argumentos.

  •  p_tcb:  Es un puntero al TCB del task que va a recibir el mensaje. Si se pasa un puntero NULL entonces el mensaje será posteado en el queue del task que invoca la función.
  • p_void: Es un puntero al mensaje que será enviado.
  • msg_size: Es el tamaño del mensaje en unidades de bytes.
  • opt: Especifica si el método de postear será FIFO or LIFO:

OS_OPT_POST_FIFO               Postear al final de la cola
OS_OPT_POST_LIFO               Postear en adelante de la cola
OS_OPT_POST_NO_SCHED   No ejecutar el scheduler después de postear.

  • p_err: Es un puntero a una variable que va a recibir un código de error retornado por esta función.

OS_ERR_NONE                                sin errores
OS_ERR_Q_MAX                             si la cola de mensajes del task está llena
OS_ERR_MSG_POOL_EMPTY    si no hay OS_MSGs disponibles

  •  Retorna    : nada

Ejemplo de Aplicación

Es hora de meter mano al código y plasmar lo estudiado, comencemos por establecer los pines de control del LCD2x16 que utilizaré en la placa Blueboard. Así mismo tendré cableado los 8 leds de los proyectos anteriores.

 

Como pueden estoy usando el puerto P1 para el manejo de los 8 leds y del LCD2x16. Así mismo para la gestion del LCD2x16 hemos creado una subcarpeta dentro de la carpeta BSP dentro de la cual están los archivos para el manejo a bajo nivel del LCD. Además creamos la carpeta de este proyecto que se llama PantallaTaskQueue:

Veamos el siguiente video donde muestro los detalles de la implementación:

Descarguen los archivos del proyecto de los siguientes links, están la carpeta BSP y la carpeta PantallaTaskQueue, cuando lo descompriman el archivo sobre la carpeta Blueboard–KeilMDK chanquen la carpeta BSP sin dudar.

4Shared

Boxnet

Henry Laredo Q.


Hoy es mi cumpleaños # 0×20

$
0
0

Quería compartir con todos los amigos del blog microembebidos esta fecha tan especial para mí. Muchas gracias por formar parte de mi vida!!!

Henry Laredo Q.



Tutorial uC/OS-III y LPC1768 – Teclado 4×4 con Task Semaphore

$
0
0

En este tutorial vamos a conocer un mecanismo de sincronización que existe en todo task y que junto con el task message queue permite entablar un mejor control de la transferencia de información entre tasks, estamos hablando del task semaphore. Para ello vamos a habilitar la gestión del teclado 4×4 de nuestra trainer para mostrar el desempeño de este servicio dentro del uC/OS-III.

Así como un Task posee un Message Queue interno, también posee un semáforo llamado Task Semaphore, el cual es creado automáticamente cuando se crea al Task. Veamos los detalles que se ciernen sobre este recurso.

Task Semaphore

En el libro del uC/OS-III existe, en la página 259, un capítulo dedicado a la sincronización de un task con un ISR (interrupt service routines) o con otro task. Se habla de dos mecanismos para realizar dicha sincronización: semáforos y banderas, de los cuales en este post vamos a tratar tan sólo de los semáforos. Dentro del mecanismo semáforo existen semáforos como objetos externos (pagina 260) y semáforos internos que son propios de un task también llamados Task Semaphore (página 275).

Como ya mencionamos, un semáforo es un mecanismo para sincronizar actividades entre un ISR y un task o entre un task y otro task. También comentamos que todo Task contiene por defecto un Task Semaphore, el cual a diferencia del Task Message Queue, no transfiere información, sino que es utilizado para señalizar la ocurrencia de algún evento específico. Se le suele representar por una bandera con una letra N que indica que puede acumular la cantidad de veces que ha sido señalizada.

Así mismo posee un contador general que establece los límites para los contadores internos de cada semáforo, estos valores pueden comprender entre 0 y 255 (8 bits), 65535 (16 bits), 4294967295 (32 bits), y está configurado por el label OS_SEM_CTR (en el archivo OS_TYPE.H). También posee un habilitador general de los servicios del semáforo llamado OS_CFG_SEM_EN (en el archivo OS_CFG.H).

Su funcionamiento es muy simple y en el libro está muy bien explicado, no obstante mencionaré brevemente lo que entendí. En la siguiente imagen podemos apreciar el comportamiento esperado con un semáforo, es decir en (1) tenemos una tarea llamda HP Task que al ejecutarse llama a la función OSSemPend() para esperar a que señalizen su semáforo interno, luego en (3) el kernel gestiona esta llamada y coloca a la función en Pending State y busca la siguiente tarea de mayor importancia para ejecutarla (5) la cual resulta ser  LP Task, más adelante un ISR (7) señaliza el semáforo del task de HP Task mediante el servicio OSSemPost(), el kernel (9) que conoce el task que estaba esperando esta señalización, al finalizar el ISR no retorna al LP Task interrumpido sino que se dirige a HP Task al ser de mayor prioridad para que continúe su ejecución. Los servicios utilizados en este ejemplo son para un semáforo externo, sin embargo la misma lógica se aplica al Task Semaphore interno.

Ahora veamos para que sirve el contador de señalizaciones. Otro comportamiento del semáforo es cuando es señalizado antes de estar siendo esperado. En la siguiente imagen tenemos un task que es interrumpido en un par de veces (3) (9) por un ISR que está señalizando mediante el servicio OSSemPost() a un determinado semáforo el cual recién es esperado en (17) por otro task, entonces es posible conocer en dicho task la cantidad de veces que el semáforo ha sido señalizado y dependerá de nosotros qué hacer con esta información. Los servicios utilizados en este ejemplo son para un semáforo externo, sin embargo la misma lógica se aplica al Task Semaphore interno.

Para terminar con esta explicación pondré cómo ejemplo el manejo de un teclado 4×4. Este proceso tiene un task que gestiona a bajo nivel las 4 entradas y 4 salidas para determinar el valor de la tecla presionada. Luego tenemos otro task que permitirá realizar una espera de boton presionado para recién validar la tecla presionada. Sea tecla_presionada una variable global, entonces cuando el primer task carga el valor encontrado en tecla_presionada, al mismo tiempo señalizará el task semaphore del segundo task y se quedará esperando a que el segundo task le vuelva a señalizar su task sempahore para estar atento a una nueva tecla presionada. El segundo task estará esperando la señalización de su semáforo para evaluar si aún no se ha soltado la tecla comparando dicha lectura con el valor cargado en la variable global tecla_presionada. Una vez que su semáforo es señalizado y confirma que se liberó la tecla, enviará el contenido de tecla_presionada al message queue del task que escribe en la pantalla LCD2x16 y señalizará al semáforo del primer task para que pueda capturar nuevas teclas presionadas en el teclado 4×4.

Esto se conoce como Bilateral Rendevouz, donde una tarea espera la señalización de otra y viceversa:

Quedando claro cómo funciona y cómo usar el task semaphore, vamos a conocer los servicios que provee el uC/OS-III para implementar su aplicación.

Los Servicios del Task Semaphore

OSTaskSemPend().- Permite bloquear el actual task hasta que su semáforo sea señalizado por otro task o un ISR, , tiene los siguientes argumentos:

  • timeout: Es la cantidad de tiempo en unidades de clock ticks que vamos a esperar por la señalización. Si la cantidad es 0 (cero) entonces esperara por siempre.
  • opt: Determina si se desea bloquear el task siempre que no exista señalización.

OS_OPT_PEND_BLOCKING
OS_OPT_PEND_NON_BLOCKING

  • p_ts: Es un puntero a una variable que recibirá el timestamp de cuando el semáforo fue señalizado o cancelado.
  • p_err: Es un puntero a una variable que va a recibir un código de error retornado por esta función.

OS_ERR_NONE                                   sin errores
OS_ERR_PEND_ABORT                   espera cancelada
OS_ERR_PEND_ISR                          si se invoco esta función desde un ISR
OS_ERR_PEND_WOULD_BLOCK  si se usa non-blocking y no hay señalización
OS_ERR_SCHED_LOCKED              si el scheduler está bloqueado
OS_ERR_STATUS_INVALID          si el pend status es inválido
OS_ERR_TIMEOUT                          si la señalización no llego dentro del timeout

  • Retorna: La actual cantidad de señalizaciones pendientes que el task semaphore ha recibido.

OSTaskSemPendAbort().- Permite cancelar la espera por una señalización y pone en estado Ready al task especificado, tiene los siguientes argumentos:

  • p_tcb: Es un puntero al task cuya espera será cancelada.
  • opt: Provee opciones para esta función:

OS_OPT_POST_NONE               Sin opción específica
OS_OPT_POST_NO_SCHED     Indica que el scheduler no será llamado

  • p_err: Es un puntero a una variable que va a recibir un código de error retornado por esta función.

OS_ERR_NONE                                 sin errores
OS_ERR_PEND_ABORT_ISR        si se invoco esta función desde un ISR
OS_ERR_PEND_ABORT_NONE   si el task no estaba esperando señales
OS_ERR_PEND_ABORT_SELF     si se intento abortar al mismo task

  • Retorna:

DEF_FALSE   si el task no estaba esperando señales o sucedió un error
DEF_TRUE    si el task señalizado fue preparado para ejecutarse por esta función

OSTaskSemPost().- Permite señalizar el semáforo de un task que esta esperando por dicha señal.

  • p_tcb: Es un puntero al TCB del task que va a recibir la señal. Si se pasa un puntero NULL entonces el task se va a señalizar a sí mismo.
  • opt: Provee opciones para la llamada:

OS_OPT_POST_NONE               sin opciones
OS_OPT_POST_NO_SCHED     evitar llamar al scheduler

  • p_err: Es un puntero a una variable que va a recibir un código de error retornado por esta función.

OS_ERR_NONE              sin errores
OS_ERR_SEM_OVF      si el post causó que se desborde el contador del semáforo

  • Retorna: El actual valor del contador del Task Semaphore señalizado.

OsTaskSemSet().- Permite establecer el contador de señales del Task Semphore.

  • p_tcb: Es un puntero al TCB del task cuyo contador se va a modificar. Si se pasa un puntero NULL entonces el contador del actual task será modificado.
  • cnt: Es el nuevo valor para el contador del semáforo.
  • p_err: Es un puntero a una variable que va a recibir un código de error retornado por esta función.

OS_ERR_NONE          sin errores
OS_ERR_SET_ISR     si se invoco esta función desde un ISR

  • Retorna: nada.

Ejemplo de Aplicación

Veamos un poco de código, vamos a seguir usando el mismo BSP con la gestión de los 8 leds, el control del lcd2x16, y a todos esto vamos a agregar el control de un teclado 4×4, el cual al presionar una de sus teclas se mostrará en la pantalla lcd2x16. Veamos los pines que voy a utilizar:

Una de las cosas nuevas es que los pines de las columnas COL1, COL2, COL3, COL4, son entradas con pull-up interno, los pines FILA1, FILA2, FILA3, FILA4 son salidas comunes y corrientes.

Así como en el post anterior, en este caso estoy agregando una nueva carpeta llamada Keypad4x4 donde estarán los archivos de gestión a bajo nivel del teclado matricial.

Vayamos con el código en el siguiente video:

Los archivos del proyecto lo pueden descargar desde los siguientes enlaces:

4Shared

Boxnet

Henry Laredo Q.


Realizando Upgrade del IAR MSP430 5.10 a la versión 5.40!!!

$
0
0

A lo largo de la existencia de los tutoriales del MSP430, realizados con el IAR v5.10, hemos tenido comunicación de problemas con versiones superiores a la v5.4 del mismo IDE, por ello he visto necesario hacer un upgrade de todos los códigos plasmados en los tutoriales para actualizarlos a la versión 5.4, dejando presente tambien los anteriores para la versión 5.10, lo cual será el retorno a primera plana del MSP430 en mi blog, ya que actualmente vengo dandole curso al RTOS uC/OS-III. Debo estar comenzándolo a subir desde ka otra semana, así que paciencia amigos.

Saludos!


Tutorial Boot C18 – Bootloader USB con la Placa Pinguino 18F4550 – Parte III

$
0
0

Llegamos a la tercera y (quizás) ultima parte del tutorial del bootloader que vendría a ser la parte más interesante de todas, ya que vamos a enseñarles cómo modificar el firmware de microchip para que funcione con algún hardware de su interés y no morir en el intento. Por favor, disfrútenlo!

Modificaciones en el Firmware del Bootloader

El firmware original (descargarlo 4shared o boxnet) MCHPUSB Bootloader PIC18F4550 Family.mcp (en el post anterior indiqué donde está ubicado) está hecho para trabajar con una placa de Microchip llamada PICDEM FS USB board la cual contiene un PIC18F4550:

Mientras que la placa pinguino tiene la siguiente distribución:

En el caso de microchip, su firmware utiliza el boton SW1 (conectado al pin RB4) y los 4 leds D2, D3, D4, D5 (conectados a los pines RD0 – RD3) para gestionar el modo bootloader. Al inicio de la operación del micro, se ejecuta el firmware del bootloader y solicita conocer el estado del pin de entrada digital RB4.

Esta verificación es sumamente importante. Si el pin RB4 es igual a 1 entonces dirige el flujo hacia el vector de reset remapeado (RM_RESET_VECTOR) que viene a ser el inicio del firmware de aplicación (no confundir con el inicio del programa del bootloader). Si no existe programa de aplicación, el micro se reiniciará denuevo al encontrar una instrucción no válida (0xFFFFFF). Posiblemente necesite resetearse la placa en caso se quede colgado al encontrar inconsistencias en la memoria de programa. Este escenario puede suceder siempre y cuando no exista datos en la región del firmware de aplicación.

Por el contrario, si el pin RB4 es igual a cero entonces el flujo continúa dentro del firmware del bootloader y se conecta al puerto USB para esperar comandos desde la PC mediante el software PDFSUSB. Todo esto está escrito dentro del archivo main.c del proyecto.

Ésta es la primera modificación en el firmware del bootloader pues como recordarán, el único botón que tiene la placa pinguino es el Reset XD, así que si queremos hacerla compatible tenemos dos opciones: colocarle por fuera un boton en otro pin del micro ya que el pin RB4 está siendo utilizado por el led RUN en el pinguino, o sino cambiar la forma de entrada al modo bootloader.

Naturalmente hemos preferido la segunda opción, para ello vamos a “moldear” el código para que se comporte como lo hace el bootloader del pinguino, es decir que después de resetear o energizar el micro, esperar 10 segundos para conectarse con el software PDFSUSB y esperar instrucciones forever, pero si pasados 10 segundos no hubo conexión entonces saltar automáticamente al programa de aplicación.

La segunda modificación necesaria es con los 4 leds de status que tiene la PICDEM, ya que en el firmware original los usa para monitorear el estado del puerto USB. En nuestro caso, tan sólo tenemos el led rojo RUN (el led verde del pinguino es el indicador de energía POWER).

La definición de estos 4 leds se encuentra en el archivo io_cfg.h (dentro de la sub carpeta include) y su aplicación está esparcida por todo el proyecto.

Para esta ocasión configuré el led RUN como único led de monitoreo, el cual durante la espera de 10 segundos parpadea cada 500mseg, luego cuando se conecta al software PDFSUSB, su parpadeo se hace más intenso como de 125mseg.

En el siguiente se podrá apreciar en sumo detalle los lugares donde se realizó las modificaciones en el bootloader de microchip para estos requerimientos:

Actualización del Firmware, versión BootV6

Como sabrán, hemos realizado una modificación para que la espera de 10 segundos funcione también sin conexión del puerto usb, aun que no lo crean recién me dí cuenta XD, cuando necesité una fuente externa para energizar cargas que quizás puedan dañar la línea de 5V del puerto USB.

Henry Laredo Q.


Actualización BootC18 – Firmware BootV6

$
0
0

He realizado una actualización del firmware bootloader, vamos por la version BootV6. Vean los cambios por favor:

He realizado una actualización, la cual permite utilizar el bootloader sin conexión USB se llama BootV6 (4shared, boxnet), … ver post

Y los cambios respectivos en el post III:

Como sabrán, hemos realizado una modificación para que la espera de 10 segundos funcione también sin conexión del puerto usb, aun que no lo crean recién me dí cuenta XD … ver post


Tutorial Boot C18 – Prender Led

$
0
0

El primer ejemplo de aplicación, con el bootloader previamente cargado en el 18F4550, será un sencillo prende led. Lo unico que requieren es conectar un led en el pin RB7 del microcontrolador siempre y cuando estén usando la placa pinguino:

El resto de detalles lo pueden apreciar en el siguiente video:

Archivos:

4Shared

Boxnet

Henry Laredo Q.


Tutorial Boot C18 – Timer0 Interrupt Buzzer

$
0
0

En este siguiente video tutorial aprenderemos cómo implementar interrupciones en los vectores remapeados del bootloader desde un código de aplicación que utiliza la interrupción del timer 0 para emitir un sonido de aviso a través de un buzzer, al mismo tiempo se mostrará un sencillo juego binario de leds.

Si tienen implementada la placa pinguino entonces necesitan lo siguiente para el ejemplo:

Para recordar cómo funciona el Timer0 de los PIC18F les sugiero revisar este enlace. Entonces cómo sabrán en el programa vamos utilizar dicho timer para emitir un sonido de keep alive con el Buzzer de tal forma que cuando esté encendido suene tan sólo un instante y se mantenga apagado por un periodo más largo, y así sucesivamente.

En primer lugar establecemos las condiciones del entorno:

Xtal Oscilador = 48MHz

Prescaler Timer 0 = 256

Para el encendido del Buzzer configuramos así el valor cargado del Timer0:

TMR0 = 0xE17B (TMR0H = 0xE1 & TMR0L = 0x7B) = 57723

Si reemplazamos los valores en la fórmula del Timer0:

periodo del encendido =  4  x  Prescaler  x  (65535 – valor cargado en timer0)
cristal oscilador

periodo del encendido =  4  x  256  x  (65535 – 57723)
48 x 10^6

periodo del encendido = 167mseg

Para el apagado del Buzzer configuramos así el valor cargado del Timer0:

TMR0 = 0

Si reemplazamos los valores en la fórmula del Timer0:

periodo del apagado =  4  x  Prescaler  x  (65535 – valor cargado en timer0)
cristal oscilador

periodo del apagado =  4  x  256  x  (65535 – 0)
48 x 10^6

periodo del apagado = 1.4seg

A continuación el video donde se detalla todo el procedimiento:

Archivos:

4Shared

Boxnet

Henry Laredo Q.


Tutorial Boot C18 – LCD2x16 con Backlight PWM

$
0
0

En este nuevo videotutorial haremos la implementación de un LCD 2×16 cuyo backlight está fijo a una frecuencia PWM generada por el pin PORTC2 del PIC18F4550. Así mismo será acompañado del keep alive del buzzer y el movimiento binario de los leds vistos en anteriores tutoriales.

Si tienen implementada la placa pinguino entonces necesitan lo siguiente para el ejemplo:

El manejo del LCD 2×16 es simple, si no recuerdan cómo funciona pueden revisar este link, tan sólo les diré que la librería lcd2x16 está en los archivos que abajo se adjuntan y cuyo manejo es muy simple, así como su configuración en los pines del micro. En el video de este tutorial verán más detalles.

En el PIC18F4550 existe el módulo ECCP (no confundir con el CCP destinado a micros de 28 pines) el cual a diferencia del módulo CCP presente en otros micros posee 2 bits extras P1M1 y P1M0 los cuales permiten configurar un tipo de salida PWM especial para aplicarlo al control de motores. En nuestro caso usaremos la configuración single output al no requerir de dichas configuraciones especiales.

El registro CCP1CON sirve para configurar el pin P1A (también llamado CCP1 para efectos de compatibilidad con los modelos de 28 pines) como una salida de tipo PWM multiplexada a través del PORTC2. En nuestro caso estamos seleccionando trabajar solamente con el pin P1A (P1M1:P1M0 = 00), estamos dejando en 1′s los 2 bits de menor peso del PWM duty cycle (DC1B1:DC1B0 = 11), y seleccionamos la función PWM para el pin PA1 y en modo activo bajo (CCP1M3:CCP1M0 = 1111) es decir que el duty cycle que configuremos será 0 (cero) y el resto 1 (uno). Lo más lógico hubiese sido hacerlo al revés, es decir seleccionar activo alto, pero quise probar con esta lógica nada más. En conclusión hicimos CCP1CON = 0x3F.

El resto de 8 bits del duty cycle están en el registro CCPR1L y son los de mayor peso, con lo cual se completan los 10 bits del duty cycle. Con estos 10 bits (CCPR1L y DC1B1:DC1B0) se obtiene el ancho del pulso para el duty cycle dentro del periodo de la señal PWM. Y cómo determino el periodo o la frecuencia de la señal PWM?

Para establecer el periodo del PWM necesitamos activar el TMR2 y asignarle un prescaler para que se compare con el registro PR2.

Entonces veamos la siguiente ecuación:

PWM Period = [(PR2) + 1] • 4 • TOSC • (TMR2 Prescale Value)

De lo cual para nuestro ejemplo estamos utilizando: TOSC = 1/48MHz, PR2 = 0xFF, TMR2 Prescale Value = 1. Reemplazando valores:

PWM Period = [(255) + 1] • 4 • (1/48×10^6) • (1)

PWM Period = 0.021 mseg

PWM Freq = 46.875 KHz

Ahora que conocemos nuestro periodo de la señal PWM podemos buscar el duty cycle deseado con la siguiente formula donde intervienen los 10 bits antes mencionados:

PWM Duty Cycle = (CCPRXL:CCPXCON<5:4>) • TOSC • (TMR2 Prescale Value)

En nuestro caso estoy aplicando lo siguiente: CCP1RL = 0xCF, DC1B1:DC1B0 = 11, entonces reemplazando valores:

PWM Duty Cycle = (11001111 11) • (1/48MHz)• (1)

PWM Duty Cycle = (0x33F) • (1/48MHz)• (1)

PWM Duty Cycle = (831) • (1/48MHz)• (1)

 PWM Duty Cycle = 0.0173125 mseg

De tal manera que el duty cycle es aproximadamente el 82% del periodo del PWM, lo cual resultará en la siguiente forma de onda la cual siendo aplicada al transistor que controla el backlight del LCD se convertirá en una atenuación del brillo:

Veamos ahora el video donde se muestra el código en pleno funcionamiento:

Archivos:

4Shared

Boxnet

Henry Laredo Q.



Tutorial Boot C18 – Teclado 4×4 con PORTB Interrupt

$
0
0

Este post trata del manejo de un teclado 4×4 y su configuración en el puerto con interrupción de cambio de estado, donde las teclas del 0 al 9 permiten controlar el brillo del backlight de una pantalla lcd 2×16.

Si han construido la placa pinguino entonces necesitan agregar los siguientes componentes:

En el desarrollo del programa verán cómo configurar la interrupción del puerto B y cómo hago el control del brillo del lcd:

Archivos:

4Shared

Boxnet

Henry Laredo Q.


NACIO LA WEB MICROEMBEBIDOS

$
0
0

Así es, acabo de anunciar oficialmente la existencia de la Web Microembebidos, actualmente estoy en el proceso de mudanza de los tutoriales al hosting donde está alojada la página. Por ello la ausencia de nuevos contenidos durante tanto tiempo, no saben cuantas cosas he tenido que aprender. Les ruego disculpen todas las molestias que esto cause, en realidad no deberían notar ninguna diferencia ya que la dinámica será la de siempre.

Así mismo les cuento que estoy cerrando las gestiones para registrar una empresa para dar servicios de asesoría mediante esta web que he creado.

Este blog está confinado a desaparecer para dar paso a la Web Microembebidos, espero verlos y leerlos ahí también. Gracias!!!


Nuevos enlaces en los tutoriales del MSP430

$
0
0

Que tal amigos, esta noticia es para anunciar que he compilado todos los tutoriales del msp430 en la versión del IAR 5.40, ya que había un problema de compatibilidad entre ésta y la verisón IAR 5.10 , felizmente ya subi todos los arhivos nuevamente y tienen disponible en cada post ambas versiones.


Los números del 2012

$
0
0

Los duendes de las estadísticas de WordPress.com prepararon un informe sobre el año 2012 de este blog.

Aquí hay un extracto:

4,329 films were submitted to the 2012 Cannes Film Festival. This blog had 36.000 views in 2012. If each view were a film, this blog would power 8 Film Festivals

Haz click para ver el reporte completo.


Nuevos tutoriales con bootloader C18 y con la placa pinguino 18F4550

$
0
0

Hemos inagurado una nueva serie de tutoriales utilizando un bootloader con lo cual mostraremos cómo implementar un bootlaoder y luego cómo escribir programas compatibles con dicho boot. Mantenganse enganchados!


Viewing all 26 articles
Browse latest View live