![]()
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.