LIBFAT: Acceso a los dispositivos de almacenamiento NOTA: Describe la antigua LIBFATLibfat es la librería que nos permite acceder a dispositivos del almacenamiento formateados en FAT12/FAT16 o FAT32, desde USB Gekko, el lector de SD interno y desde hace un tiempo, dispositivo USB (este último limitado a las especificaciones USB 1.1)
Para integrarla en tus programas, basta con añadir -lfat como librería en el Makefile (mis ejemplos, la incluyen) y #include <fat.h> en tus fuentes.
UTF-8 frente a ANSI char setAntes de meternos en faena con la librería, conviene conocer un aspecto que puede daros alguna que otra sorpresa. Cuando los ordenadores iniciaron su andadura, se hizo necesario emplear un estándar para poder manejar texto e intercambiar la información y el estándar elegido fue el ASCII que se definió entre 1963-1966 y posteriormente, se redefinió en 1986 dando lugar a la especificación ANSI que conocemos actualmente.
ASCII se componía de 128 caracteres, usando 7 bits por tanto, donde se le añadía un último bit como bit de paridad en las transmisiones, dado que éste estándar se pensó inicialmente para telegrafía.
De esos 128 caracteres, los primeros 33 se utilizaban para códigos de control no imprimibles. Así por ejemplo, chr 8 representa un espacio atrás, chr 9 el tabulador, chr 10 avance de línea , chr 13 el retorno de carro y chr 32 el espacio. De los imprimibles, el caracter '0' es chr 48, la 'A' es chr 65, y la 'a' chr 97
En algunos sistemas (LINUX/UNIX) el carácter 13 se toma como avance de línea y retorno de carro de forma simultanea y esa es la razón por las que algunas veces, si abrimos un README desde el Wordpad de Windows, el texto se apelotona
(porque espera que las líneas terminen con los caracteres 10 y 13)
Sin embargo, pronto se hizo necesario ampliar el set de caracteres, dado que ASCII recogía los caracteres latinos USA pero por ejemplo, no recogía la ñ, ni los caracteres de acentuación que nosotros conocemos. Esto se recoge cómo una extensión que ocupa los caracteres desde el 128 al 255 (usando lo que antes era un bit de paridad) conocido como estándar ISO-8859-1 y así en solo 8 bits, se recogían todos los caracteres especiales de origen latino.
screenlib utiliza una captura de caracteres que contiene 224 caracteres (elimina los 32 primeros caracteres ASCII, pero conserva el carácter espacio) y que recogen esa especificación ANSI con los caracteres extendidos ISO-8859-1
Puesto que estos estándares solo recogen caracteres latinos, se hizo necesario crear otros estándares que recogieran otros tipos de caracteres para soportar otros idiomas, como por ejemplo, el ruso y así nació el Unicode. Dentro del Unicode existe una especificación llamada Utf-8 (8-bit Unicode Transformation Format) que utiliza un sistema de longitud variable para que partiendo de 8 bits, se puedan codificar todos los caracteres Unicode.
Esta especificación es la que utiliza la librería libfat a la hora de trabajar con nombres de ficheros, pero ¿como nos afecta?
Pues bien, Utf-8 utiliza el bit de mayor peso, el que actualmente recoge los caracteres ISO-8859-1 para cambiar y codificar la tabla de caracteres a utilizar. Y al ser de longitud variable, un carácter puede ocupar de 1 a 4 bytes
En Unicode, los primeros 256 caracteres son los mismos que los especificados en la ISO-8859, por lo que los caracteres Utf-8 del 0 al 127, corresponderían directamente a lo que nosotros conocemos. Lo que cambia es la forma de tratar el bit de mayor peso y eso compromete los nombres de los ficheros que utilicen acentos, etc.
Así que tenéis dos opciones:
1) O bien procuráis utilizar nombres de ficheros/directorios que no usen caracteres fuera del rango de 128 caracteres definido por el estandar ANSI
2) O si no os queda más remedio, tendréis que convertir de Utf-8 a ISO-8859 para imprimir o de ISO-8859 a Utf-8 para manejar nombres de ficheros/directorios que usen esos caracteres especiales.
En mi caso en mi aplicación Wiireader, a mi sólo me interesa ver el nombre de los ficheros correctamente al visualizarlos en pantalla, dado que al listar ficheros desde libfat, los obtengo en Utf-8 y por tanto, al abrirlos conservaré ese Utf-8
Así pues, como sabemos que los caracteres del 0 a 127 en Utf-8 son iguales y ocupan solo un byte, solo nos restaría saber como se codifican los caracteres del 128 al 255 para poder imprimirlos correctamente desde la
screenlibEn
http://es.wikipedia.org/wiki/UTF-8 podemos ver que para cubrir el rango de caracteres 0x80 a 0x7ff (nosotros solo necesitamos de 0x80 a 0xff), se utilizan dos bytes:
rango: 000080 - 0007FF Unicode (UTF-16) 00000xxx xxxxxxxx Utf-8: 110xxxxx 10xxxxxx
Así pues, podríamos hacer una rutina de conversión de Utf-8 a ISO-8859 de esta forma:
void UTF8_To_ISO8859(u8 *src, *u8 dst)
{
while(1)
{
if(src[0]==0) {*dst=0;break;} // fin de cadena
if((src[0] & 128)==0) {*dst++=*src++;} // rango de 0x0 a 0x7f
else
if((scr[0] & 224) ==192 && (scr[1] & 192)==128) // rango 0x80 a 0x7ff
{
if(src[0] & 0x1c) {*dst=0;break;} // error fuera del rango 0x80 a 0xff: caracteres no soportados
*dst++= (((src[0] & 3)<<6) | (src[1] & 63)); src+=2;
}
else // error fuera del rango de 0x0 a 0x7ff o no usa especificación UTF-8
{
*dst=0;break;
}
}
}
Con esta rutina (que por cierto, acabo de crear aquí y no ha sido testeada :twisted:) podréis convertir cadena utf-8 como fuente, a otra ISO como destino, para visualizarla con s_printf por ejemplo (caso de que el nombre quepa en pantalla
). Mi sugerencia sería que ampliases esa función para que en caso de ser un carácter no soportado, visualizara un carácter comodín (? por ejemplo), y que trabajases con Utf-8 siempre y solo convirtieras para visualizar (pero allá cada uno
)
***************************************************************************************************
LIBFAT: InicializandoLibfat se inicializa con ésta función:
bool fatInit (u32 cacheSize, bool setAsDefaultDevice);
Lo normal es que la inicialiceis así:
fatInit(8, false);
El 8 establece una caché de 8 clusters para entradas de directorios y cosas así.
Libfat trata de montar una serie de dispositivos en unidades con nombres como "fat0:","fat1:",..., "fat4:"... pero permite asignar uno de esos dispoitivos cómo unidad por defecto "fat:".
Lo normal es que esa unidad la asignéis a la SD, de ésta forma:
fatSetDefaultInterface(PI_INTERNAL_SD);
En fat.h podéis ver una serie de dispositivos definidos como PI_algo, como PI_SDGECKO_A, o PI_USBSTORAGE.
Sin embargo, si vais a acceder a varios dispositivos fat, es preferible utilizar el nombre original de la "partición" a estar intercambiado el interface por defecto.
Asi pues, imaginemos que tenemos esta cadena:
char name[]="fatX:/test.txt";
podríamos especificar la unidad directamente, modificando la cadena de ésta forma: name[3]=48+PI_INTERNAL_SD; (48 es el carácter ASCII '0')
Detectando las particiones y asignando el tamaño de la caché de lecturaBien, con lo que tenemos hasta ahora, podríamos funcionar sin problemas, pero por cuestiones de velocidad de lectura, puede resultar conveniente asignar un tamaño interno de caché para la lectura de ficheros. Al mismo tiempo, resulta conveniente detectar si los dispositivos están operativos, pues ciertas operaciones podrían colgar directamente la consola.
Para asignar el tamaño de la cache de lectura, se utiliza ésta función:
bool fatEnableReadAhead(PARTITION_INTERFACE partitionNumber, u32 numPages, u32 pageSize);
Para no liaros mucho, os diré que:
fatEnableReadAhead(PI_INTERNAL_SD, 12, 32);
o
fatEnableReadAhead(PI_USBSTORAGE, 12, 32);
Son unos buenos valores. El tamaño de página y el número de paginas, requieren un uso de memoria que si no se administra bien, se desperdicia inútilmente, por lo que no es conveniente abusar.
Quizá estés pensando ¿por que no nos explica mas a fondo el uso de éstas funciones? Pues por que el autor original de la librería, ha decidido desechar los cambios que tanto rodries cómo yo introdujimos y está inmerso en una tarea de despedazar la librería y migrar fuentes, no se muy bien si con la idea de jugar al despiste o que: solo puedo decir que repite viejos errores y arrastra ciertos bugs que darán problemas, sobre todo en multithread. Y que yo no pienso mover un dedo ni para reportarlos, ni para migrar mis cambios, ni nada de nada, puesto que para mí la libfat que usamos funciona bien y no me da la gana volver atrás, sobre todo en el tema de la caché. Pero obviamente, ésto es mi opción y si tu "actualizas" la librería, éstas funciones de caché no las encontrarás.
Antes de asignar un tamaño de cache, conviene saber si el dispositivo está en funcionamiento:
{
DIR_ITER *dir;
char path[]="fatX:/";
int have_device=0;
path[3]=48+PI_INTERNAL_SD;
dir = diropen(path);
if (dir) {dirclose(dir); {have_device|=1;fatEnableReadAhead(PI_INTERNAL_SD, 12, 32);}
}
De esta forma, podemos saber si el dispostivo SD está operativo (intentando abrir el directorio raiz) y en caso afirmativo, ajustamos un flag de presencia
y activamos la caché de lectura.
Los dispositivos USB suelen causar problemas: a veces tardan mucho en inicializarse, no se resetean bien o se tropiezan o yo que se, pero hay veces que les da la neura y eso explica cosas raras que suelo hacer en mis inicializaciones.
Por ejemplo, en Wiireader esto es lo que uso yo para inicializar libfat tratando de pillar dispositivo SD y dispositivo USB:
fatInit(8, false);
sleep(2); // espero dos segundos, no se muy bien si para darle tiempo a que el dispositivo negocie o esté preparado para funcionar
fatSetDefaultInterface(PI_INTERNAL_SD); // usa SD como fat:
have_device=0; // bits que me indican la presencia de dispositivos
{
path_file[3]=48+PI_INTERNAL_SD; // path_file ="fatX:/";
for(n=0;n<5;n++) // numero de reintentos por si la unidad no está preparada o está "enojada"
{
dir = diropen(path_file);
if (dir) {dirclose(dir); have_device|=1;fatEnableReadAhead(PI_INTERNAL_SD, 12, 32);break;} // monta la cache
usleep(200*1000);
}
path_file[3]=48+PI_USBSTORAGE; // path_file ="fatX:/";
for(n=0;n<5;n++) // numero de reintentos por si la unidad no está preparada o está "enojada"
{
dir = diropen(path_file);
if (dir) {dirclose(dir); have_device|=2;fatEnableReadAhead(PI_USBSTORAGE, 12, 32);break;} // monta la cache
usleep(200*1000);
}
}
¿Que alguno lo ve superfluo? Pues tal vez, pero cuando uno está hasta los cojones de que entras desde un RESET y no te ve el "penedrive", sales y entras y ahora sí y cosas así, acabas por ver demonios por todas partes y si haciéndolo así, no te pasa ¿que harías tu?
***************************************************************************************************
LIBFAT: Trabajando con ficheros y directoriosListando directoriosPara listar ficheros y directorios,puedes utilizar el siguiente procedimiento:
#include "sys/dir.h"
.....
DIR_ITER * dir;
static char namefile[256*4]; // reserva espacio suficiente para UTF-8
static struct stat filestat;
dir = diropen("fat:/"); // abre el directorio raiz de la unidad fat:
while(1)
{
if(dirnext(dir, namefile, &filestat)!=0) break; // si no hay mas entradas en el directorio, sal
if(filestat.st_mode & S_IFDIR) // es el nombre de un directorio
{
// namefile contiene el nombre del directorio en formato UTF-8,que puede ser "." o ".." tambien
}
else
{
// namefile contiene el nombre del fichero en formato UTF-8
}
}
dirclose(dir); // cierra el directorio
Operaciones con ficherosDespués de iniciar libfat y comprobar que los dispositivos están operativos, podéis utilizar las librerías estándar ANSI-C referente a dispositivos para crear/borrar/renombrar/fijar directorios.
Por ejemplo con:
mkdir("fat3:/jamacuco", S_IREAD | S_IWRITE);
Se crearía el directorio "jamacuco" en raiz de la SD, fijando los correspondientes permisos. Quizá no sea buena idea usar fat3 así directamente, pues si algún lumbreras cambia el orden de los dispositivos mañana , fat3 quizá apunte a otra cosa (de ahi eso de usar puntero[3]=48+PI_torreo)
Para leer/escribir ficheros, podéis utilizar fopen/fread/fwrite/fseek/ftell/fclose...
Vamos, que si tienes alguna duda sobre las librerías estándar, pásate por aquí y te lo miras más a fondo
:
http://c.conclase.net/librerias/index.phpEl Desmontaje de dispositivosResulta conveniente asegurarse antes de salir de un programa de que todos los ficheros abiertos, se han cerrado y de que las cachés de escritura se han "flusheado". Resulta conveniente desmontar los dispositivos:
Eso se puede hacer añadiendo esto a la función de salida que definimos con at_exit():
if(have_device & 1) fatUnmount(PI_INTERNAL_SD); // desmonta la SD
if(have_device & 2) fatUnmount(PI_USBSTORAGE); // desmonta dispostivo USB
Estas funciones las modifiqué para que flushearán los datos incluso en el caso de que se detectaran ficheros abiertos antes de salir y no se pudieran desmontar los dispositivos. Si cierras los ficheros, se flushearán los buffers modificados, pero existen ciertas operaciones como crear directorios, borrarlos, etc, que pueden dejar alguna operación pendiente de escritura desde la caché al dispositivo y por eso es aconsejable desmontarlo.
Y con esto y un bizcocho, tienes todo lo necesario para trabajar con ficheros
***************************************************************************************************
Gestión del TiempoFormas de perder el tiempoEn: #include <unistd.h>
Podemos encontrar dos interesantes funciones para "perder" el tiempo:
unsigned sleep(unsigned int __seconds ); // espera X segundos durmiendo el hilo
int usleep(useconds_t __useconds); // espera X microsegundos durmiendo el hilo
Ejemplos:
sleep(2); // espera dos segundos
usleep(2*1000); // espera 2 milisegundos
La explicación correcta de ésta función sería: "Suspende la ejecución del hilo (del programa) permitiendo que otro hilo pueda tomar el control y al cabo de X tiempo, trata de continuar con la ejecución del hilo en cuanto sea posible".
Es decir, que al margen de la precisión que pueda tener el temporizador, el tiempo que tarda el hilo en volver a despertar, dependerá de si hay un hilo con mayor prioridad que esté funcionando en el momento de cumplirse el plazo o no.
Existe una función interna en la librería libogc, que no está incluida en ningún fichero de cabecera (de ahí que la denomine interna) llamada udelay, que podeis declararla así:
void udelay(int us); // retarda X microsegundos
Esta función pierde el tiempo de forma similar a usleep, pero no duerme el hilo. La importancia de este matiz la podréis comprender cuando hable de la programación multithread pero os basta con saber que si un hilo no suspende (o duerme) su ejecución, no puede ser interrumpido por otro hilo salvo que ese nuevo hilo tenga una prioridad mayor. Luego queda patente que usleep() permite la ejecución de hilos que estén a la espera de inferior o igual prioridad, mientras que udelay() no.
Medida Relativa del tiempoHay una función llamada gettick() a nivel interno que nos devuelve un contador de 32 bits de tiempo. Esta función no está presente en ningún fichero de cabecera (si os interesa curiosear el fuente de las funciones, está en libogc/timesupp.c) por lo que tendréis que definirla así:
unsigned gettick();
gettick() devuelve "ticks" que hay que convertir a medidas de tiempo humanas.
Así podemos encontrar en /ogc/lwp_watchdog.h una serie de definiciones para realizar esa conversión:
ticks_to_secs(ticks);
ticks_to_millisecs(ticks);
ticks_to_microsecs(ticks);
ticks_to_nanosecs(ticks);
En el ejemplo 3 yo incluyo un procedimiento para medir el tiempo en ms de ésta forma:
#define ticks_to_msecs(ticks) ((ticks)/TB_TIMER_CLOCK)
extern unsigned gettick();
unsigned get_ms_clock() // retorna los milisegundos transcurridos
{
return ticks_to_msecs(gettick());
}
Así podemos medir el intervalo de tiempo entre dos medidas, haciendo la diferencia y usarlo para hacer los ajustes necesarios en el programa (por ejemplo, actualizar movimientos de nuestros sprites de forma independiente al refresco de la pantalla y cosas asi)
Sin embargo, existe una función que hace más o menos lo mismo que gettick(), pero trabajando con medidas de 64 bits en lugar de los 32 bits que devuelve gettick() y que se define en /ogc/lwp_watchdog.h
long long gettime();
Te lo menciono aquí por si necesitas precisión extra, pero para mi uso particular, con gettick() me es suficiente (y parece que gettime() gasta un tiempo en hacer la lectura)
Reloj de Tiempo RealEn libogc/timesupp.c hay una función:
int clock_gettime(struct timespec *tp);
Esta función devuelve 0 si todo fue bien y los datos en una estructura timespec que se define así:
struct timespec {
time_t tv_sec; /* Seconds */ // unsigned long (u32)
long tv_nsec; /* Nanoseconds */ // (s32)
};
Yo no he empleado ésta función nunca, pero de entrada, le falta un parámetro con respecto a la definición de la función en time.h
y supongo que equivale a clock_gettime(CLOCK_REALTIME, &time); en otros sistemas.
En fin, lo dejo dicho para que vosotros lo sepáis: eso si, éste tipo de funciones no están pensadas para ser llamadas en cada frame, porque suelen ser lentas al acceder al reloj de tiempo real, así que haced un uso responsable de ella.
Timers programablesEl sistema te permite definir una serie de alarmas de tiempo que vienen muy bien para trabajar con pasos regulares. Echad un ojo a ogc/system.h
Hasta ahora hemos visto funciones que nos permiten perder el tiempo y otras que nos permiten medir un intervalo, pero en ninguna se puede estar seguro de que el tiempo transcurrido es el esperado, (dependiendo de la precisión del temporizador, claro)
Por ello las alarmas son especialmente útiles ya que te permiten programar un tiempo que una vez transcurrido, produce una interrupción que nos lleva a una callback de tratamiento donde teniendo cuidado, eso sí (recordemos que estamos en tiempo de interrupción), podemos ajustar lo que sea necesario.
Desde esa callback por ejemplo, podemos "despertar" hilos de una forma regular (yo por ejemplo, empleo una alarma en mi juego Guitarfun para actualizar graficos/leer pad cada 20 ms, mientras el hilo de fondo, descodifica y reproduce Ogg y de esa forma, el programa trabaja de forma independiente al refresco de frames (50/60 Hz) para cada formato de imagen)
La creación de una alarmaPara crear una alarma, primero necesitamos dos estructuras, una para alojar la alarma:
syswd_t myalarm;
Y otra para alojar el tiempo de la alarma:
struct timespec alarm_time;
Entonces podemos proceder a crear una alarma con:
s32 SYS_CreateAlarm(syswd_t *thealarm);
De esta forma:
SYS_CreateAlarm(&myalarm);
la función devuelve 0 si la alarma se creó correctamente, pero ahora toca ponerla en funcionamiento con alguna de estas dos funciones:
s32 SYS_SetAlarm(syswd_t thealarm,const struct timespec *tp,alarmcallback cb); // una sola vez
s32 SYS_SetPeriodicAlarm(syswd_t thealarm,const struct timespec *tp_start,const struct timespec *tp_period,alarmcallback cb); // periódicamente
La primera ajusta la alarma para un único uso pero nada impide para que dentro de la callback programes de nuevo la alarma usando SetAlarm con el mismo u otro tiempo y repetir (o en cualquier otro punto). La segunda usa un temporizador inicial y luego otro que se utilizará para llamar de forma periódica a la callback
-
thealarm: Estructura alarm inicializada con SYS_CreateAlarm()
-
tp, tp_start, tp_periodic: Estructuras timespec con el tiempo a programar.
-
cb: Callback que será llamada al transcurrir el tiempo
Devuelven 0 si se pudo ajustar la alarma.
Veamos algunos ejemplos:
Ejemplo de Alarma de una sola vez:syswd_t myalarm;
struct timespec alarm_time;
void alarm_handler()
{
// aqui llega cuando se cumpla el tiempo: recuerda que esto es una callback de interrupcion
}
......
alarm_time.tv_sec=0;
alarm_time.tv_nsec=20*1000*1000; // 20 milisegundos (notese que tv_nsec es en nanosegundos)
SYS_CreateAlarm(&myalarm);
SYS_SetAlarm(myalarm,&alarm_time,alarm_handler);
Ejemplo de Alarma Autoprogramable:syswd_t myalarm;
struct timespec alarm_time;
int flip=0;
void alarm_handler()
{
// aqui llega cuando se cumpla el tiempo: recuerda que esto es una callback de interrupcion
alarm_time.tv_sec=0;
if(flip)
alarm_time.tv_nsec=20*1000*1000; // 20 milisegundos (notese que tv_nsec es en nanosegundos)
else
alarm_time.tv_nsec=40*1000*1000; // 40 milisegundos (notese que tv_nsec es en nanosegundos)
flip^=1;
SYS_SetAlarm(myalarm,&alarm_time,alarm_handler); // reprogramamos la alarma desde la callback
}
......
alarm_time.tv_sec=0;
alarm_time.tv_nsec=20*1000*1000; // 20 milisegundos (notese que tv_nsec es en nanosegundos)
SYS_CreateAlarm(&myalarm);
SYS_SetAlarm(myalarm,&alarm_time,alarm_handler);
Ejemplo de Alarma Periódica:syswd_t myalarm;
struct timespec alarm_time;
struct timespec alarm_time2;
int flip=0;
void alarm_handler()
{
// aqui llega cuando se cumpla el tiempo: recuerda que esto es una callback de interrupcion
// aqui llega despues de dos segundos y luego, cada 20 milisegundos
}
......
alarm_time.tv_sec=2; // 2 segundos
alarm_time.tv_nsec=0;
alarm_time2.tv_sec=0;
alarm_time2.tv_nsec=20*1000*1000; // 20 milisegundos (notese que tv_nsec es en nanosegundos)
SYS_CreateAlarm(&myalarm);
SYS_SetPeriodicAlarm(myalarm,&alarm_time, &alarm_time2, alarm_handler); // aguarda 2 segundos y a apartir de ahí, llama la callback cada 20 ms
Liberando/Cancelando alarmasLas alarmas que podemos programar, son finitas. No se cual será el número exacto pero obviamente,lo puedes comprobar creando alarmas hasta que se produzca un error
La función:
s32 SYS_CancelAlarm(syswd_t thealarm);
La puedes usar para cancelar alarmas ya programadas y en curso, mientras que la función:
s32 SYS_RemoveAlarm(syswd_t thealarm);
Te permite liberar las alarmas creadas con SYS_CreateAlarm();
Y con esto ya tienes lo necesario para gestionar el tiempo de forma apropiada
***************************************************************************************************
Programación MultihiloEn Wii la programación multihilo consiste en la posibilidad de interrumpir la ejecución de un hilo para tratar otro o en la posibilidad de aprovechar los tiempos muertos de un hilo, para gestionar otras cosas desde otro hilo.
Los milagros no existen y la CPU sólo puede ocuparse de uno de ellos en cada momento, por lo que no se trata de ejecutar varias tareas de forma simultánea, si no de aprovechar los tiempos muertos como digo, o tratar una serie de eventos conectados a las interrupciones desde un hilo, de forma que ese hilo interrumpe a otro para realizar alguna tarea y al mismo tiempo, permite el paso de las interrupciones.
Los hilos tienen asignados todos un nivel de prioridad. Esta prioridad se utiliza para determinar que hilo toma el control y la regla consiste en que el hilo que tiene mayor prioridad y está en espera de ser activado, se hace con el control.
Una cosa que tenéis que tener en cuenta y manejar con sumo cuidado, es el tema de las reentradas en las funciones. Incluso el tema de utilizar funciones del mismo grupo dentro de una librería o que comparten recursos.
Por ejemplo, si desde un hilo se leen los pads con WPAD_ScanPads() y desde otro se comprueban los datos leídos del Wiimote, es posible que el hilo que lee el Wiimote tome el control en medio de una llamada a WPAD_ScanPads() y reciba datos erróneos y en otros casos, se pueden obtener una serie de paradojas.
Si tu función utiliza únicamente registros del procesador y variables locales o globales que no se modifican, no tendrás problemas en utilizarla en multihilo, puesto que
cada hilo tiene su propia pila y los registros se salvan antes de cambiar de hilo. Pero si tu función accede a variables globales que se modifican, acceden a registros del hardware o a dispositivos, en definitiva todo lo que sea compartir recursos, entonces no estará preparada para un acceso multihilo y deberías protegerla mediante uso de semáforos (que veremos luego).
La creación de un hiloLa función LWP_CreateThread() es la encargada de crear los hilos (ver ogc/lwp.h):
s32 LWP_CreateThread(lwp_t *thethread,void* (*entry)(void *),void *arg,void *stackbase,u32 stack_size,u8 prio);
-
thethread: Puntero de tipo lwp_t que recibe el handle. Por ejemplo: static lwp_t h_my_thread=LWP_THREAD_NULL;
-
entry: Función de entrada del hilo
-
arg: Puntero con el argumento que recibirá la función de entrada del hilo (para asignar datos privados). Puede ser NULL
-
stackbase: Puntero con la dirección base de la pila (alineada a 32). Si NULL, se asigna de forma interna.
-
stack_size: Tamaño de la pila en bytes. Si 0 se asigna 8KB por defecto.
-
prio: Prioridad del hilo, de 0 a 127 (maxima prioridad). Como referencia el player ogg utiliza 80 y yo suelo asignar a 40 el main() (no conozco la prioridad de main() pero tampoco es que importe mucho
).
La función devuelve 0 si no hubo problema alguno.
La función de entrada del hiloLa función de entrada sería algo así:
void * thread_main(void *arg)
{
// arg recibe el puntero arg que asignamos al crear el hilo
return NULL; // aqui se sale del hilo
}
Evidentemente, sería un hilo muy fugaz y su utilidad sería bastante baja. La cosa mejoraría añadiéndole un bucle de control y algo que permitiera oxigenar el resto de hilos:
int exit_thread_main=0;
void * thread_main(void *arg)
{
// arg recibe el puntero arg que asignamos al crear el hilo
while(1)
{
usleep(20000); // aguarda 20 ms
if(exit_thread_main) break;
/* aqui haces lo que te de la gana */
}
return NULL; // aqui se sale del hilo
}
Aquí observamos que este hilo ha sido pensado para tener un prioridad mayor que la de main(), para que mas o menos, cada 20 milisegundos se active (usleep() como ya expliqué, duerme el hilo y luego trata de despertarlo) y haga lo que tenga que hacer en un bucle infinito (luego la tarea sea haría cada 20 ms+ lo que tarde el resto, que puede ser una burrada de tiempo), pero que tiene una variable,
exit_thread_main la cual asignándola a un valor no cero, rompe el bucle y eso haría que finalizara el hilo.
Ésta variable de control es muy importante puesto que este hilo por si solo no va a poder salir y eso daría lugar a un cuelgue de la máquina cuando pretendas volver al HBC por ejemplo. Así pues, antes de volver deberías asignar exit_thread_main=1; y asegurarte que el hilo está 'despierto' para que se produzca ese break.
Controlando la ejecución del hiloEn la función thread_main de antes, hemos observado un método que supone que el hilo va a su bola desactivándose por espacio de 20 ms y luego volviéndose activar. Sin embargo, seguramente te resultará mas útil activar el hilo de forma mas precisa, desde una callback de una alarma o cualquier otro sitio.
Para eso nos será muy útil el uso de colas:
static lwpq_t thread_queue=LWP_TQUEUE_NULL; // thread_queue contendrá el handler de la cola
Inicializamos la cola antes de crear el hilo con:
LWP_InitQueue(&thread_queue); // inicializa el queue
Después creamos el hilo y en la función de entrada del hilo ponemos:
int exit_thread_main=0;
void * thread_main(void *arg)
{
// arg recibe el puntero arg que asignamos al crear el hilo
while(1)
{
if(exit_thread_main) break; // si éste es nuevo :D
LWP_ThreadSleep(thread_queue); // duerme el hilo usando una cola para poder despertarlo
if(exit_thread_main) break;
/* aqui haces lo que te de la gana */
}
return NULL; // aqui se sale del hilo
}
Como se puede apreciar, ahora el hilo estará controlado por esa cola y necesitaremos decirle desde fuera que puede continuar.
Para ello, desde una callback podéis usar ésta función:
LWP_ThreadSignal(thread_queue);
Al hacerlo, en el momento en que las reglas de prioridad lo permitan, nuestro hilo despertará, hará su tarea y al completar el bucle, volverá a dormirse en espera de que llamemos a LWP_ThreadSignal() de nuevo.
Por poneros algunos ejemplos, en mi programa Guitarfun se usa el método de las colas para actualizar los gráficos cada 20 ms mediante una alarma, los reproductores de audio esperan la activación del hilo de ésta forma para rellenar con samples los buffers, etc.
La técnica de usleep() que usábamos al principio, la uso en Guitarfun también, para el nuevo hilo que se encarga de rellenar los buffers de lectura de los ficheros Ogg, en lecturas de 256KB, tarea que puede consumir varios segundos y que por tanto, es despreciable la pérdida de ms del usleep y precisa un funcionamiento independiente.
Para finalizar una cola se usa:
void LWP_CloseQueue(lwpq_t thequeue);
Salir de los HilosYa hemos visto que si retornamos desde la función de inicio del hilo, salimos de él, pero si queremos salir de un hilo desde el principal o vamos a salir al sistema, conviene seguir los siguientes pasos:
exit_thread_main=1; // asignamos a 1 la variable que rompe el bucle while()
LWP_ThreadSignal(thread_queue); // si estamos usando colas, forzamos que el hilo despierte en caso de estar dormido en la cola
LWP_JoinThread(h_my_thread, NULL); // pasamos el handle del hilo (lwp_t) del que esperaremos su finalización
LWP_CloseQueue(thread_queue); // cerramos la cola
Si algo fallara aquí, la aplicación se colgaría, así que conviene asegurarse de que ninguna otra cosa bloqueará la salida del hilo.
No he preparado ejemplos con uso de múltiples hilos, pero mi juego Guitarfun o mi aplicación Wiireader, muestran varios ejemplos que podéis estudiar.
Tened mucho cuidado eso si, con usar funciones que se interfieran con el uso de varios hilos
Cambiando la prioridadCómo habéis visto, en Wii los hilos obedecen a temas prioridad a la hora de activarse, siendo el hilo de mayor prioridad el más abusón de la clase, no permitiendo la ejecución de otros hilos salvo que el mismo se libere.
Algunas veces, resulta interesante cambiar la prioridad de un hilo para forzar que tome el control y luego bajársela, para permitir que otro le pueda interrumpir o simplemente, fijar una prioridad muy alta para que otro hilo no pueda interrumpir al hilo actual hasta que acabe de hacer algo, momento en el cual restableceremos la prioridad.
Por desgracia en Wii hay un problema raro que afecta a la programación multihilo y parece ser que a los números flotantes. Dicho problema se corrigió parcialmente, pero no del todo, así que puede ser conveniente proteger ciertas secciones de ésta manera.
La función para cambiar la prioridad es:
void LWP_SetThreadPriority(lwp_t thethread,u32 prio);
-
thethread: Handle del hilo. Si LWP_THREAD_NULL se trata del hilo actual (no confundir con NULL). También puedes usar LWP_GetSelf(); para conocer el Handle del hilo actual.
-
prio: Nueva prioridad para el hilo
Así por ejemplo, si dentro del main() ponemos LWP_SetThreadPriority(LWP_THREAD_NULL, 40); asignamos a main() la prioridad 40
Nota: no existe una función equivalente para conocer la prioridad
Proteger secciones críticasYa hemos dicho que cambiando la prioridad, podríamos proteger una determinada sección de un hilo contra el cambio de hilos solicitado desde una callback (interrupciones), pero hay puntos donde por conveniencia es mejor deshabilitar las propias interrupciones.
Para ello podemos contar con dos funciones, presentes en ogc/irq.h:
u32 IRQ_Disable(); // deshabilita las interrupciones y devuelve el estado
void IRQ_Restore(u32 level); // restaura las interrupciones con el estado devuelto por IRQ_Disable()
Vamos que esto se usa así:
u32 stat;
stat=IRQ_Disable(); // deshabilita
/* hacer algo que no lleve mucho tiempo, ni requiera interrupciones habilitadas */
IRQ_Restore(stat); // restaura
El uso de semáforosLos semáforos se utilizan para regular el paso de los hilos hacia una función o funciones que solo permiten el acceso a un hilo cada vez, debido a una serie de características propias de esa función o funciones. Por ejemplo, si voy a leer desde USB, puedes programar el dispositivo para que te lea un sector y el dispositivo tardará un tiempo en "servírtelo", pero no puedes leer sectores no consecutivos de forma simultanea, ni el dispositivo puede atender mas peticiones hasta que haya finalizado con la primera (ya que solo tiene "dos manos" hablando virtualmente
).
Así pues, se necesita un mecanismo que haga que si dos o mas hilos acceden a una función protegida, solo uno pase y el resto queden a la espera hasta que finalice el hilo que pasó primero. Y de eso se encargan los semáforos (un ejemplo lo tenéis en LIBFAT, que bloquea el acceso a todas sus funciones mediante semáforos)
Para crear uno, primero tenemos que crear un identificador tal que así (ver ogc/mutex.h):
mutex_t mi_semaforo=LWP_MUTEX_NULL;
La función para inicializarlo es ésta:
s32 LWP_MutexInit(mutex_t *mutex,boolean use_recursive);
Lo inicializamos así:
LWP_MutexInit(&mi_semaforo, false);
El semáforo creado devolverá 0 si se inició correctamente y permitirá el paso de forma inicial.
Cuando queramos eliminarlo, de ésta forma (lo explico ahora, que si nó, luego me olvido
)
LWP_MutexDestroy(mi_semaforo);
Bien, ya tenemos nuestro semáforo creado ¿Y ahora cómo se usa en la práctica?
Pues para eso tenemos éstas dos funciones:
s32 LWP_MutexLock(mutex_t mutex); // bloquea mediante el semáforo
s32 LWP_MutexUnlock(mutex_t mutex); // desbloquea el semáforo
Pongamos un ejemplo:
int my_funcion(int manolo)
{
int ret=0;
LWP_MutexLock(mi_semaforo); // bloquea el paso de hilos. De forma inicial al crear el semáforo, permite el paso de un hilo
/* codigo protegido por el semaforo */
......
......
LWP_MutexUnlock(mi_semaforo); // desbloquea el semaforo y permite el paso del hilo siguiente a la función
return ret;
}
Como se puede apreciar, es un mecanismo muy sencillo: aplicándolo a todas las funciones de una librería que sean accesibles, podemos evitar el acceso de otros hilos a una librería mientras esté en uso por parte de uno de ellos, pero esto no se traduce en un error de forma que la librería diga algo así "lo siento, pero estoy en uso: vuelva usted mas tarde", si no que el semáforo lo convierte en "en esté momento la función está ocupada, aguarde en la sala de espera hasta que el hilo pepito abandone la función".
Pero claro, es responsabilidad del hilo "pepito" informar con MutexUnlock de que otros hilos pueden acceder a ella. y también es importante que el hilo "pepito" evite volver a pasar por MutexLock, puesto que ya no dispone de tarjeta de visita y será bloqueado también.
Y ésto es todo lo que tenéis que saber sobre los semáforos, los hilos y el copón de vino para trabajar con hilos. El resto es cosa vuestra.
FIN