Estoy tratando de crear un RTOS semi-preventivo (cooperativo) para microcontroladores PIC x16. En mi pregunta anterior , aprendí que acceder al puntero de la pila de hardware no es posible en estos núcleos. Miré esta página en PIClist, y esto es lo que estoy tratando de implementar usando C.
Mi compilador es Microchip XC8 y actualmente estoy trabajando en un PIC16F616 con oscilador RC interno de 4MHz seleccionado en los bits de configuración.
Aprendí que puedo acceder a los registros PCLATH y PCL con C, mirando el archivo de encabezado de mi compilador. Entonces, traté de implementar un conmutador de tareas simple.
Funciona como se desea en el depurador si detengo el depurador después de reiniciar, reiniciar y configurar la PC en el cursor cuando el cursor no está en la primera línea ( TRISA=0;
) sino en otra línea (por ejemplo ANSEL=0;
). En el primer inicio del depurador recibo estos mensajes en Debugger Console
:
Launching
Programming target
User program running
No source code lines were found at current PC 0x204
Editar: no sé qué hizo que funcionara, pero el depurador ahora funciona perfectamente. Por lo tanto, omita el resultado y el párrafo anteriores.
Editar: cambiar la definición principal de esta manera hace que el código a continuación funcione. Esto inicia la función principal en la dirección del programa 0x0099
. No sé qué causa esto. Esta no es una solución real. Ahora supongo que hay un error específico del compilador.
void main(void) @ 0x0099
{
Aquí está mi código C:
/*
* File: main.c
* Author: abdullah
*
* Created on 10 Haziran 2012 Pazar, 14:43
*/
#include <xc.h> // Include the header file needed by the compiler
__CONFIG(FOSC_INTOSCIO & WDTE_OFF & PWRTE_ON & MCLRE_OFF & CP_OFF & IOSCFS_4MHZ & BOREN_ON);
/*
* INTOSCIO oscillator: I/O function on RA4/OSC2/CLKOUT pin, I/O function on RA5/OSC1/CLKIN
* WDT disabled and can be enabled by SWDTEN bit of the WDTCON register
* PWRT enabled
* MCLR pin function is digital input, MCLR internally tied to VDD
* Program memory code protection is disabled
* Internal Oscillator Frequency Select bit : 4MHz
* Brown-out Reset Selection bits : BOR enabled
*/
/*
* OS_initializeTask(); definition will copy the PCLATH register to the task's PCLATH holder, which is held in taskx.pch
* This will help us hold the PCLATH at the point we yield.
* After that, it will copy the (PCL register + 8) to current task's PCL holder which is held in taskx.pcl.
* 8 is added to PCL because this line plus the "return" takes 8 instructions.
* We will set the PCL after these instructions, because
* we want to be in the point after OS_initializeTask when we come back to this task.
* After all, the function returns without doing anything more. This will initialize the task's PCLATH and PCL.
*/
#define OS_initializeTask(); currentTask->pch = PCLATH;\
currentTask->pcl = PCL + 8;\
asm("return");
/*
* OS_yield(); definition will do the same stuff that OS_initializeTask(); definition do, however
* it will return to "taskswitcher" label, which is the start of OS_runTasks(); definition.
*/
#define OS_yield(); currentTask->pch = PCLATH;\
currentTask->pcl = PCL + 8;\
asm("goto _taskswitcher");
/*
* OS_runTasks(); definition will set the "taskswitcher" label. After that it will change the
* current task to the next task, by pointing the next item in the linked list of "TCB"s.
* After that, it will change the PCLATH and PCL registers with the current task's. That will
* make the program continue the next task from the place it left last time.
*/
#define OS_runTasks(); asm("_taskswitcher");\
currentTask = currentTask -> next;\
PCLATH = currentTask->pch;\
PCL = currentTask->pcl;
typedef struct _TCB // Create task control block and type define it as "TCB"
{
unsigned char pch; // pch register will hold the PCLATH value of the task after the last yield.
unsigned char pcl; // pcl register will hold the PCL value of the task after the last yield.
struct _TCB* next; // This pointer points to the next task. We are creating a linked list.
} TCB;
TCB* currentTask; // This TCB pointer will point to the current task's TCB.
TCB task1; // Define the TCB for task1.
TCB task2; // Define the TCB for task2.
void fTask1(void); // Prototype the function for task1.
void fTask2(void); // Prototype the function for task2.
void main(void)
{
TRISA = 0; // Set all of the PORTA pins as outputs.
ANSEL = 0; // Set all of the analog input pins as digital i/o.
PORTA = 0; // Clear PORTA bits.
currentTask = &task1; // We will point the currentTask pointer to point the first task.
task1.next = &task2; // We will create a ringed linked list as follows:
task2.next = &task1; // task1 -> task2 -> task1 -> task2 ....
/*
* Before running the tasks, we should initialize the PCL and PCLATH registers for the tasks.
* In order to do this, we could have looked up the absolute address with a function pointer.
* However, it seems like this is not possible with this compiler (or all the x16 PICs?)
* What this compiler creates is a table of the addresses of the functions and a bunch of GOTOs.
* This will not let us get the absolute address of the function by doing something like:
* "currentTask->pcl=low(functionpointer);"
*/
fTask1(); // Run task1 so that we get the address of it and initialize pch and pcl registers.
currentTask = currentTask -> next; // Point the currentTask pointer to the next pointer which
fTask2(); // is task2. And run task2 so that we get the correct pch and pcl.
OS_runTasks(); // Task switcher. See the comments in the definitions above.
}
void fTask1(void)
{
OS_initializeTask(); // Initialize the task
while (1)
{
RA0 = ~RA0; // Toggle PORTA.0
OS_yield(); // Yield
RA0 = ~RA0; // Toggle PORTA.0
}
}
void fTask2(void)
{
OS_initializeTask(); // Initialize the task
while (1)
{
RA1 = ~RA1; // Toggle PORTA.1
OS_yield(); // Yield
RA1 = ~RA1; // Toggle PORTA.1
}
}
Y aquí está el archivo de lista de desmontaje que creó mi compilador. Comienza en line 74
.
He programado el chip real y ningún cambio en PORTA; no funciona
¿Cuál es la razón por la que mi programa no funciona?
Lo que está tratando de hacer es complicado, pero muy educativo (si está preparado para gastar mucho esfuerzo).
Primero, debe darse cuenta de que este tipo de cambio de tareas solo para PC (a diferencia de PC+SP) (que es lo único que puede hacer en un núcleo PIC simple de 12 o 14 bits) solo funcionará cuando todo el rendimiento ( ) las declaraciones en una tarea tienen la misma función: no pueden estar en una función llamada, y el compilador no debe haber interferido con la estructura de la función (como podría ocurrir con la optimización).
Próximo:
currentTask->pch = PCLATH;\
currentTask->pcl = PCL + 8;\
asm("goto _taskswitcher");
Una solución a su problema de PCLATH es declarar una etiqueta después del goto y escribir los bits inferior y superior de esa etiqueta en sus ubicaciones pch y pcl. Pero no estoy seguro de que pueda declarar una etiqueta 'local' en el ensamblaje en línea. Seguro que puedes en MPASM simple (Olin sonreirá).
Por último, para este tipo de cambio de contexto, debe guardar y restaurar TODO el contexto del que podría depender el compilador, que podría incluir
La arquitectura PIC es más problemática a este respecto porque muchos recursos se ubican en todo el mapa de memoria, mientras que las arquitecturas más tradicionales los tienen en registros o en la pila. Como consecuencia, los compiladores de PIC a menudo no generan código reentrante, que es lo que definitivamente necesita para hacer las cosas que desea (nuevamente, Olin probablemente sonreirá y ensamblará).
Si te gusta esto por el placer de escribir un conmutador de tareas, te sugiero que cambies a una CPU que tenga una organización más tradicional, como ARM o Cortex. Si está atascado con los pies en una placa de concreto de PIC, estudie los conmutadores de PIC existentes (por ejemplo, ¿salvo/pumkin?).
Examiné la lista de ensamblaje que proporcionó, y nada salta como obviamente roto.
Si yo fuera tú, mis próximos pasos serían:
(1) Elegiría algún otro método para hacer parpadear los LED. El notorio "problema de lectura-modificación-escritura" puede (o no) ser provocado por el "XORWF PORTA, F" en la lista de ensamblaje.
Tal vez algo como:
// Partial translation of code from abdullah kahraman
// untested code
// feel free to use however you see fit
void fTask2(void)
{
OS_initializeTask(2); // Initialize task 2
while (1)
{
PORTC = 0xAA;
OS_yield(2); // Yield from task 2
PORTC = 0x55;
OS_yield(2); // Yield from task 2
}
}
(Si realmente desea ver explicaciones detalladas sobre por qué "XORWF PORTA, F" a menudo causa problemas, consulte " ¿Qué causa que al encender un solo pin de salida en Microchip PIC16F690 se apague espontáneamente otro pin en el mismo puerto? "; " Qué sucede cuándo se escriben los datos en LATCH? "; " El problema de lectura, modificación y escritura "; " Leer antes de escribir " )
(2) Pasaría un solo paso por el código, asegurándome de que las variables se establezcan en los valores esperados y en la secuencia esperada. No estoy seguro de si existe un depurador de hardware de un solo paso para el PIC16F616, pero hay muchos simuladores de microcontroladores PIC excelentes , como PICsim , que pueden simular chips de la serie PIC16.
El código de un solo paso (en un simulador o con un depurador de hardware de un solo paso) es una buena manera de comprender los detalles de lo que realmente está sucediendo, confirmar que las cosas están sucediendo de la manera prevista y le permite ver las cosas que son prácticamente imposible de ver cuando se ejecuta el programa a toda velocidad.
(3) Si todavía estoy perplejo, intentaría traducir el código para usar matrices en lugar de punteros. Algunas personas encuentran que el uso de punteros es un poco complicado y difícil de depurar. A menudo me doy cuenta de que, en el proceso de traducir el código de puntero engañoso en código orientado a matrices, descubro cuál es el error. Incluso si termino volviendo al código de puntero original y descarto la versión de matriz, el ejercicio es útil porque me ayudó a encontrar y corregir el error. (A veces, el compilador puede generar un código más corto y más rápido a partir de un código orientado a matrices, por lo que hay ocasiones en las que desecho el código de puntero original y me quedo con la versión de matriz).
Tal vez algo como
// Partial translation of code from abdullah kahraman
// untested code
// feel free to use however you see fit
struct TCB_t // Create task control block and type define it as "TCB_t"
{
unsigned char pch; // PCLATH value
unsigned char pcl; // PCL value
int next; // This array index points to the next task. We are creating a linked list.
};
int currentTask = 1; // This TCB index will point to the current task's TCB.
struct TCB_t tasks[3]; // Define the TCB for task1 and task2.
#define OS_initializeTask(x); tasks[x].pch = PCLATH;\
tasks[x].pcl = PCL + 8;\
asm("return");
#define OS_runTasks(); asm("_taskswitcher");\
currentTask = tasks[currentTask].next;\
PCLATH = tasks[currentTask].pch;\
PCL = tasks[currentTask].pcl;
#define OS_yield(x); tasks[x].pch = PCLATH;\
tasks[x].pcl = PCL + 8;\
asm("goto _taskswitcher");
Básicamente estaría de acuerdo con davidcary. Parece que podría funcionar.
No sé qué hizo que funcionara, pero el depurador ahora funciona perfectamente.
Supongo que con esto quieres decir que funciona perfectamente en el simulador .
1) Verifique que sus tareas funcionen por sí solas, en un entorno que no sea RTOS en el chip real.
2) Hacer depuración en circuito. Recorra el programa en el chip real y observe todas las variables relevantes para asegurarse de que todo va según lo planeado.
Solo miré tu código brevemente, pero no tiene sentido. En varios lugares, está escribiendo en PCL y luego espera que ejecute otras instrucciones después de eso.
Como también dije antes, C es inapropiado para este tipo de acceso de bajo nivel a los registros fundamentales del hardware. Realmente necesitas usar ensamblador para esto. Tratar de descubrir por qué el código C no funciona es solo una pérdida de tiempo sin sentido.
A continuación se muestra la forma de hacerlo con el ensamblaje en línea utilizando el compilador XC8, ¡y funciona ahora! Sin embargo, necesito agregar desarrollar más código para guardar y restaurar el STATUS
registro, lo que parece un poco más complicado de lo que es para un registro normal.
Editar: el código ha cambiado. Consulte las versiones anteriores de esta publicación para ver el código anterior.
/*
* File: main.c
* Author: abdullah
*
* Created on 10 Haziran 2012 Pazar, 14:43
*/
#include <xc.h> // Include the header file needed by the compiler
#include "RTOS.h" // Include the header for co-operative RTOS.
__CONFIG(FOSC_INTOSCIO & WDTE_OFF & PWRTE_ON & MCLRE_OFF & CP_OFF & IOSCFS_4MHZ & BOREN_ON);
unsigned char OS_currentTask; // This register holds the current task's place in the array OS_tasks
unsigned char OS_tasks[4]; // This array holds PCL and PCLATH for tasks. This array will have..
// .. (number of tasks)*2 elements, since every task occupies 2 places.
void fTask1(void); // Prototype the function for task1.
void fTask2(void); // Prototype the function for task2.
void main(void)
{
TRISA = 0; // Set all of the PORTA pins as outputs.
TRISC = 0; // Set all of the PORTC pins as outputs.
ANSEL = 0; // Set all of the analog input pins as digital i/o.
PORTA = 0; // Clear PORTA bits.
PORTC = 0; // Clear PORTC bits.
OS_currentTask = 0; // Current task is first task.
fTask1(); // Call task to initialize it.
OS_currentTask += 2; // Increment task pointer by two since every task occupies 2 places in the array.
fTask2(); // Call task to initialize it.
OS_runTasks(4); // Run the tasks in order. The argument of this macro takes is: (Number of tasks) * 2
}
void fTask1(void)
{
OS_initializeTask(); // Initialize the task so that task runner can get its ingredients.
while (1)
{
PORTC = 0xAA;
OS_yield(); // Yield CPU to other tasks.
PORTC = 0x55;
OS_yield(); // Yield CPU to other tasks.
}
}
void fTask2(void)
{
OS_initializeTask(); // Initialize the task so that task runner can get its ingredients.
while (1)
{
PORTC = 0xFF;
OS_yield(); // Yield CPU to other tasks.
PORTC = 0x00;
OS_yield(); // Yield CPU to other tasks.
}
}
Y aquí está el archivo de encabezado RTOS.h
:
/*
* File: RTOS.h
* Author: abdullah
*
* Created on 21 Haziran 2012 Perşembe, 10:51
*/
#ifndef RTOS_H
#define RTOS_H
asm("OS_yield MACRO");
asm("local OS_tmp");
asm("movlw _OS_tasks ; Store the address of tasks, which is the start address of our task 'array'.");
asm("addwf _OS_currentTask, w ; Add current task's index to the start address.");
asm("movwf fsr ; We have the index of current task in W. Copy it to FSR");
asm("movlw high(OS_tmp) ; Copy PCLATH register's contents for the label, to W register.");
asm("movwf indf ; Copy W to current task's first item. We now store PCLATH of the current state of the task.");
asm("incf fsr, f ; Increment index, so that we will point to the next item of current task.");
asm("movlw low(OS_tmp) ; Copy PCL of the label to W register. This will let us save the PCL of the current state of the task.");
asm("movwf indf ; Copy W to task's next item. With that, we will initialize the current task.");
asm("goto OS_taskswitcher");
asm("OS_tmp: ; We will use this label to gather the PC of the return point.");
asm("ENDM");
#define OS_yield(); asm("OS_yield");
asm("OS_initializeTask MACRO");
asm("local OS_tmp");
asm("movlw _OS_tasks ; Store the address of tasks, which is the start address of our task 'array'.");
asm("addwf _OS_currentTask, w ; Add current task's index to the start address.");
asm("movwf fsr ; We have the index of current task in W. Copy it to FSR");
asm("movlw high(OS_tmp) ; Copy PCLATH register's contents for the label, to W register.");
asm("movwf indf ; Copy W to current task's first item. We now store PCLATH.");
asm("incf fsr,f ; Increment index, so that we will point to the next item of current task.");
asm("movlw low(OS_tmp) ; Copy PCL of the label to W register. This will let us save the PCL of the current state of the task.");
asm("movwf indf ; Copy W to task's next item. With that, we will initialize the current task.");
asm("return ; We have gathered our initialazation information. Return back to main.");
asm("OS_tmp ; We will use this label to gather the PC of the return point.");
asm("ENDM");
#define OS_initializeTask(); asm("OS_initializeTask");
asm("OS_runTasks MACRO numberOfTasks");
asm("global OS_taskswitcher");
asm("OS_taskswitcher:");
asm("CLRWDT");
asm("movlw 0x02 ; W = 2");
asm("addwf _OS_currentTask, f ; Add 2 to currentTask, store it in currentTask.");
asm("movlw numberOfTasks ; W = numOfTasks");
asm("subwf _OS_currentTask, w ; w= f - w");
asm("btfsc status, 0 ; If currentTask >= numOfTasks");
asm("clrf _OS_currentTask ; Clear currentTask");
asm("movlw _OS_tasks ; Store the address of tasks, which is the start address of our task 'array'.");
asm("addwf _OS_currentTask, w ; Add current task's index to the start address.");
asm("movwf fsr ; We have the index of current task in W. Copy it to FSR");
asm("movf indf, w ; Copy the contents of current task's first item to W");
asm("movwf pclath ; Copy W to PCLATH. As a result, current task's PCLATH will be in PCLATH register.");
asm("incf fsr, f ; Increment index, so that we will point to the next item of current task.");
asm("movf indf, w ; Copy the contents of current task's second item to W.");
asm("movwf pcl ; Copy W to PCL. Finally, current task's PCL will be in PCL register.");
asm("ENDM");
#define OS_runTasks(numberOfTasks); asm("OS_runTasks "#numberOfTasks);
#endif /* RTOS_H */
A continuación se muestra cómo implementar esto usando ensamblador. Acceda al mismo código con formato (enlaces a Pastebin) . ¿Cómo puede ser mejorado? Este es mi primer programa en ensamblador de PIC, cualquier comentario es apreciado.
list p=16f616
#include p16f616.inc
;*** Configuration Bits ***
__CONFIG _FOSC_INTOSCIO & _WDTE_OFF & _WDT_OFF & _PWRTE_ON & _MCLRE_OFF & _CP_OFF & _IOSCFS_8MHZ & _BOREN_ON
;**************************
;*** Variable Definitions ***
VARS UDATA ; Define undefined data(s).
numOfTasks res 1 ; This variable holds the number of tasks multiplied by 2.
currentTask res 1 ; Index variable that points to the current task's index in "tasks"
tasks res 4 ; This is task "array". Every task occupies 2 bytes.
;****************************
;*** Reset Vector ***
RESET CODE 0x0000 ; Define a code block starting at 0x0000, which is reset vector, labeled "RESET"
goto start ; Start the program.
;********************
;*** Main Code ***
MAIN CODE
start ; Label the start of the program as "start".
banksel TRISA ; Select appropriate bank for TRISA.
clrf TRISA ; Clear TRISA register. Configure all of the PORTA pins as digital outputs.
clrf TRISC ; Clear TRISC register. TRISC and TRISA are at the same bank, no need for "banksel".
clrf ANSEL ; Clear ANSEL register and configure all the analog pins as digital i/o.
banksel PORTA ; Select appropriate bank for PORTA.
clrf PORTA ; Clear PORTA register.
clrf PORTC ; Clear PORTC register. PORTC and PORTA are at the same bank, no need for "banksel".
movlw 0x04 ; W = Number of tasks * 2.
movwf numOfTasks ; Since every task has two datas in it, we will multiply by 2.
clrf currentTask ; Set the task#0 as current task.
CALL task0 ; Call task#0 since we need to initialize it. We are going to get..
; ..its PCL and PCLATH values at the start address.
movlw 0x02 ; W = 2
addwf currentTask, f ; Increment currentTask by 2, since every task occupies 2 places.
CALL task1 ; Call task#1, for initialazation.
taskswitcher
movlw 0x02 ; W = 2
addwf currentTask, f ; Add 2 to currentTask, store it in currentTask.
movf numOfTasks, w ; W = numOfTasks
subwf currentTask, w ; w= f - w
btfsc STATUS, 0 ; If currentTask >= numOfTasks
clrf currentTask ; Clear currentTask
movlw tasks ; Store the address of tasks, which is the start address of our task "array".
addwf currentTask, w ; Add current task's index to the start address.
; For example; task1's index is 2: [task0_1][task0_2][task1_1][task1_2]....
; 0 1 2 3
movwf FSR ; We have the index of current task in W. Copy it to FSR
movf INDF, w ; Copy the contents of current task's first item to W
movwf PCLATH ; Copy W to PCLATH. As a result, current task's PCLATH will be in PCLATH register.
incf FSR, f ; Increment index, so that we will point to the next item of current task.
movf INDF, w ; Copy the contents of current task's second item to W.
movwf PCL ; Copy W to PCL. Finally, current task's PCL will be in PCL register.
goto $ ; This instruction is not effective. But, enter the endless loop.
;*** TASK 0 ***
TASK0 CODE
;**************
task0
movlw tasks ; Store the address of tasks, which is the start address of our task "array".
addwf currentTask, w ; Add current task's index to the start address.
movwf FSR ; We have the index of current task in W. Copy it to FSR
movf PCLATH, w ; Copy PCLATH register's contents to W register.
movwf INDF ; Copy W to current task's first item. We now store PCLATH.
incf FSR,f ; Increment index, so that we will point to the next item of current task.
movlw low($+3) ; Copy PCL+3 to W register. This will let us save the PCL of the start of the task.
movwf INDF ; Copy W to task's next item. With that, we will initialize the current task.
return ; We have gathered our initialazation information. Return back to main.
task0main
banksel PORTA ; Select the appropriate bank for PORTA
movlw 0xAA ; Move literal to W so that W = 0xAA
movwf PORTA ; PORTA = 0xAA. Use a LATA register to create more robust code.
movlw tasks ; Store the address of tasks, which is the start address of our task "array".
addwf currentTask, w ; Add current task's index to the start address.
movwf FSR ; We have the index of current task in W. Copy it to FSR
movf PCLATH, w ; Copy PCLATH register's contents to W register.
movwf INDF ; Copy W to current task's first item. We now store PCLATH of the current state of the task.
incf FSR,f ; Increment index, so that we will point to the next item of current task.
movlw low($+3) ; Copy PCL+3 to W register. This will let us save the PCL of the current state of the task.
movwf INDF ; Copy W to task's next item. With that, we will initialize the current task.
goto taskswitcher ; Yield the CPU to the awaiting task by going to task switcher.
banksel PORTA ; Select the appropriate bank for PORTA
movlw 0x55 ; Move literal to W so that W = 0x55
movwf PORTA ; PORTA = 0xAA. Use a LATA register to create more robust code.
goto task0main ; Loop by going back to "task0main". We will continuously toggle PORTA.
;*** TASK 1 ***
TASK1 CODE
;**************
task1
movlw tasks ; Store the address of tasks, which is the start address of our task "array".
addwf currentTask, w ; Add current task's index to the start address.
movwf FSR ; We have the index of current task in W. Copy it to FSR
movf PCLATH, w ; Copy PCLATH register's contents to W register.
movwf INDF ; Copy W to current task's first item. We now store PCLATH.
incf FSR,f ; Increment index, so that we will point to the next item of current task.
movlw low($+3) ; Copy PCL+3 to W register. This will let us save the PCL of the start of the task.
movwf INDF ; Copy W to task's next item. With that, we will initialize the current task.
return ; We have gathered our initialazation information. Return back to main.
task1main
banksel PORTA ; Select the appropriate bank for PORTA
movlw 0xAA ; Move literal to W so that W = 0xAA
movwf PORTA ; PORTA = 0xAA. Use a LATA register to create more robust code.
movlw tasks ; Store the address of tasks, which is the start address of our task "array".
addwf currentTask, w ; Add current task's index to the start address.
movwf FSR ; We have the index of current task in W. Copy it to FSR
movf PCLATH, w ; Copy PCLATH register's contents to W register.
movwf INDF ; Copy W to current task's first item. We now store PCLATH of the current state of the task.
incf FSR,f ; Increment index, so that we will point to the next item of current task.
movlw low($+3) ; Copy PCL+3 to W register. This will let us save the PCL of the current state of the task.
movwf INDF ; Copy W to task's next item. With that, we will initialize the current task.
goto taskswitcher ; Yield the CPU to the awaiting task by going to task switcher.
banksel PORTA ; Select the appropriate bank for PORTA
movlw 0x55 ; Move literal to W so that W = 0x55
movwf PORTA ; PORTA = 0xAA. Use a LATA register to create more robust code.
goto task1main ; Loop by going back to "task1main". We will continuously toggle PORTA.
END ; END of the program.
abdullah kahraman
abdullah kahraman
abdullah kahraman
Wouter van Ooijen
abdullah kahraman
Wouter van Ooijen