Download presentation
Presentation is loading. Please wait.
Published byCamilla Paul Modified over 9 years ago
1
Copyright © 2004 Texas Instruments. All rights reserved. 1.Introduction 2.Real-Time System Design Considerations 3.Hardware Interrupts (HWI) 4.Software Interrupts (SWI) 5.Task Authoring (TSK) 6.Data Streaming (SIO) 7.Multi-Threading (CLK, PRD) 8.BIOS Instrumentation (LOG, STS, SYS, TRC) 9.Static Systems (GCONF, TCONF) 10.Cache (BCACHE) 11.Dynamic Systems (MEM, BUF) 12.Flash Programming (HexAIS, Flashburn) 13.Inter-Thread Communication (MSGQ,...) 14.DSP Algorithm Standard (XDAIS) 15.Input Output Mini-Drivers (IOM) 16.Direct Memory Access (DMA) 17.Review DSP/BIOS System Integration Workshop 1 T TO Technical Training Organization
2
Objectives Semaphores (SEM) have been used in prior chapters to synchronize threads, and are a common approach in many systems. While there are times that the SEM is a good choice, there are other situations in which their use can lead to problems. In this chapter, a variety of inter-thread communication and synchronization examples will be considered employing a variety of BIOS thread comm API, including: LCKMBX QUEATM MSGQ 2 T TO Technical Training Organization
3
Overview ATMAtomic Fxns SEMSemaphore LCKLock MBXMailbox QUEQueue MSGQMessage Queue Inter-Thread Comm & Synch 3 T TO Technical Training Organization
4
Critical Section Resource Protection Pass a copy of resources to 2 nd thread No possibility of conflict, allows both to use concurrently Doubles storage requirements, adds copy time Assign concurrent threads to the same priority – FIFO +No possibility of conflict, no memory/time overhead, easy - Forces involved threads to be same priority Disable interrupts during critical sections +Needed if a hardware interrupt shares data - Affects response time of all threads in the system Disable SWI/TSK scheduler during critical section +Assures one SWI/TSK to finish with resource before another begins -Affects the response times of all other SWIs Raise priority of SWI/TSK during critical section *Set priority to highest priority which may access the resource +Equal priority SWIs/TSKs run in FIFO order, avoiding competition -Can affect response times of SWIs/TSKs of intervening priority Use atomic functions on shared resources +Imposes minimal jitter on interrupt latencies - Only allows minimal actions on shared resources Regulate access rights via Semaphores +No conflict or memory/time overhead of passing copy -Multiple semaphore schemes can introduce problems 4 T TO Technical Training Organization
5
Inter-Thread Comm & Synch Overview ATMAtomic Fxns SEMSemaphore LCKLock MBXMailbox QUEQueue MSGQMessage Queue 5 T TO Technical Training Organization
6
DSP/BIOS Atomic Functions Allows thread to manipulate variables without interrupt intervention Are C callable functions optimized in assembly Allows reliable use of global variables shared between SWI and HWI ATM_decATM_inc - Good for inter-thread counters ATM_clearATM_set - Counters - to start/restart - Generic pass of value between threads ATM_andATM_or - Perform boolean op’s on shared variables - Good for event flags, etc a TSK... x = x+1; LD reg, x ADD reg, 1 ST x, reg... HWI... for(x...) x = 0;... x 10 11? 10 / 0 11 6 T TO Technical Training Organization
7
Inter-Thread Comm & Synch Overview ATMAtomic Fxns SEMSemaphore LCKLock MBXMailbox QUEQueue MSGQMessage Queue 7 T TO Technical Training Organization
8
time B B A A A and B same priority, A is ready before B SEM_pend(semObj) SEM_post(semObj) Priority=1 time Synchronization Semaphore B higher priority than A, B is ready before A B B A A SEM_ pend(semObj) block! SEM_post (semObj) preempted! Precondition for B Depends on A Not dependent on A Priority=2 Priority=1 8 T TO Technical Training Organization
9
Not dependent on A Semaphores and Priority B B A A Both B and C depend on A B pends on the semaphore first, then C When A posts, B runs first because it pended first Semaphores use a FIFO Queue for pending tasks! SEM_ pend(&semObj) block! SEM_post (&semObj) preempted! Precondition for B and C Depends on A Priority=1 time C C SEM_ pend(&semObj) block! Priority=2 interrupt! 9 T TO Technical Training Organization
10
Mutual Exclusion Semaphore Two or more tasks need concurrent access to a serial reusable resource Mutual exclusion using semaphore Initialize semaphore count to 1 pend before accessing resource - lock out other task(s) post after accessing resource - allow other task(s) Problems may occur when tasks compete for more than one resource Priority inversion Deadlock Void task0() {SEM_pend(&semMutex); `critical section` SEM_post(&semMutex); } Void task0() {SEM_pend(&semMutex); `critical section` SEM_post(&semMutex); } Void task1() {SEM_pend(&semMutex); `critical section` SEM_post(&semMutex); } Void task1() {SEM_pend(&semMutex); `critical section` SEM_post(&semMutex); } Task1 Task0 SEM_pend(& semMutex ) SEM_pend(& semMutex ) block! SEM_post(& semMutex ) preempted! SEM_post(& semMutex ) Priority=2 Priority=1 10 T TO Technical Training Organization
11
Priority Inversion A B C D A B interrupt! Post(mutex) preempted Time Priority lower higher post (mutex) Pend(mutex) blocks Initially Mutex = 1 Pend(mutex) High-priority tasks block while waiting for lower-priority tasks to relinquish semaphore “The failure turned out to be a case of priority inversion” — Mars Pathfinder Flight Software Cognizant Engineer 11 T TO Technical Training Organization
12
Inversion Solution: Priority Inheritance A A pend(mutex) Interrupt readies B B time Priority lower higher setpri(A,newpri) post(mutex), setpri(A,oldpri) C D A pend(mutex) post(mutex) Elevate task priority before calling accessing resource Lower task priority after accessing resource Question: Do we even need to use semaphore in this situation? No Do not use TSK_yield() if semaphore is removed 12
13
Deadlock static Void TaskA() {SEM_pend(&res_1); // use resource1 Task_A may get stuck here SEM_pend(&res_2); // use resource2 SEM_post(&res_1); SEM_post(&res_2); } static Void TaskA() {SEM_pend(&res_1); // use resource1 Task_A may get stuck here SEM_pend(&res_2); // use resource2 SEM_post(&res_1); SEM_post(&res_2); } static Void TaskB() {SEM_pend(&res_2); // use resource2 Task_B may get stuck here SEM_pend(&res_1); // use resource1 SEM_post(&res_2); SEM_post(&res_1); } static Void TaskB() {SEM_pend(&res_2); // use resource2 Task_B may get stuck here SEM_pend(&res_1); // use resource1 SEM_post(&res_2); SEM_post(&res_1); } Also known as deadly embrace Tasks cannot complete because they have blocked each other TaskA and TaskB require the use of resource 1 and 2. Neither task will release a resource until it is completed Conditions for deadlock to occur: Mutual exclusion : Access to shared resource protected with mutual exclusion SEM Circular pend: Circular chain of tasks hold resources that are needed by others in the chain (cyclic processing) Multiple pend and wait: Tasks lock more than one resource at a time Preemption: Tasks needing mutual exclusion are at a different priorities 13 T TO Technical Training Organization
14
Deadlock: Detect, Recover, Eliminate Difficult to detect May happen infrequently Use timeouts in blocking API; monitor timeout via SYS_error Monitor SWI with implicit STS Recover is not easy Reset the system Rollback to a pre-deadlock state Solution Careful design Rigorous testing Eliminating Deadlock: Remove one of these conditions Mutual exclusion: Make resources sharable Circular pend: Set a particular order Multiple pend and post: Lock only one resource at a time or all resources that will be used (starvation potential) Preemption: Assign tasks that need mutual exclusivity to the same priority Better: Use more sophisticated BIOS API 14 T TO Technical Training Organization
15
Inter-Thread Comm & Synch LCK_createLCK_pend LCK_deleteLCK_post LCK API Overview ATMAtomic Fxns SEMSemaphore LCKLock MBXMailbox QUEQueue MSGQMessage Queue 15 T TO Technical Training Organization
16
Nested Semaphore Calls: LCK Void Task_A() { SEM_pend(&semUser); funcInner(); SEM_post(&semUser); } Void Task_A() { SEM_pend(&semUser); funcInner(); SEM_post(&semUser); } Void funcInner() { SEM_pend(&semUser); // use resource guarded by sem SEM_post(&semUser); } Void funcInner() { SEM_pend(&semUser); // use resource guarded by sem SEM_post(&semUser); } Unrecoverable blocking call Void Task_A() { LCK_pend(&lckUser); funcInner(); LCK_post(&lckUser); } Void Task_A() { LCK_pend(&lckUser); funcInner(); LCK_post(&lckUser); } Void funcInner() { LCK_pend(&lckUser); // use resource guarded by lck LCK_post(&lckUser); } Void funcInner() { LCK_pend(&lckUser); // use resource guarded by lck LCK_post(&lckUser); } Use of SEMaphore with nested pend yields permanent block Use of LCK (Lock) with nested pend avoids blockout BIOS MEM Manager and selected RTS functions internally use LCK, can cause TSK switch 16 T TO Technical Training Organization
17
Inter-Thread Comm & Synch + mailbox message can be any desired structure + semaphore signaling built in (read and write) + allows multiple readers and/or writers - fixed depth of messaging - copy based – 2 copies made in/out of MBX MBX_pend MBX_post MBX_create MBX_delete MBX NOTE: The MBX API are not related to the mailbox component of the SWI object! Overview ATMAtomic Fxns SEMSemaphore LCKLock MBXMailbox QUEQueue MSGQMessage Queue 17 T TO Technical Training Organization
18
Void writer(Void) { MsgObjmsg; IntmyBuf[SIZE];... msg.addr = myBuf; msg.len = SIZE*sizeof(Int); MBX_post(&mbx, &msg, SYS_FOREVER);... } Example: Passing Buffer Info Via Mailbox Void reader(Void) { MsgObjmail; Intsize, *buf;... MBX_pend(&mbx, &mail,SYS_FOREVER); buf = mail.addr; size = mail.len;... } typedef struct MsgObj { Int len; Int *addr; }; handle to msg obj * msg to put/get timeout MBX_post - add message to end of mailbox MBX_pend - get next message from mailbox block until mail received or timeout block if MBX is already full 18 T TO Technical Training Organization
19
Creating Mailbox Objects MBX.OBJMEMSEG = prog.get(“ISRAM"); var myMBX = MBX.create("myMBX"); myMBX.comment = "my MBX"; myMBX.messageSize = 1; myMBX.length = 1; myMBX.elementSeg = prog.get(“IRAM"); Message Object creation via TCONF hMbx = MBX_create(msgsize, mbxlen, attrs); MBX_delete(hMbx); Dynamic Message Object Creation struct MBX_Attrs { Int segid; } Message Object creation via GCONF Message Size = MADUs per message Mailbox Length = max # messages queued 19 T TO Technical Training Organization
20
Inter-Thread Comm & Synch + any number of messages can be passed + message can be anything desired (beginning with QUE_elem) + atomic API are provided to assure correct sequencing - no semaphore signaling built in Overview ATMAtomic Fxns SEMSemaphore LCKLock MBXMailbox QUEQueue MSGQMessage Queue 20 T TO Technical Training Organization
21
msg1msg2msg3 QUE_Obj struct MyMessage { QUE_Elem elem; first field for QUE Int x[1000]; array/structure sent not copy based! } Message1; typedef struct QUE_Elem { struct QUE_Elem *next; struct QUE_Elem *prev; } QUE_Elem; typedef struct QUE_Elem { struct QUE_Elem *next; struct QUE_Elem *prev; } QUE_Elem; Queues : QUE QUE message is anything you like, starting with QUE_Elem QUE_Elem is a set of pointers that BIOS uses to manage a double linked list Items queued are NOT copied – only the QUE_Elem ptrs are managed! QUE_put(hQue,*msg3) add message to end of queue ( writer ) *elem = QUE_get(hQue) get message from front of queue ( reader ) msg1msg2msg3 QUE_Obj How do you synchronize reader and writer? 21 T TO Technical Training Organization
22
QUE API Summary QUE_putAdd a message to end of queue – atomic write QUE_getGet message from front of queue – atomic read QUE_enqueueNon-atomic QUE_put QUE_dequeueNon-atomic QUE_get QUE_headReturns ptr to head of queue (no de-queue performed) QUE_emptyReturns TRUE if queue has no messages QUE_nextReturns next element in queue QUE_prevReturns previous element in queue QUE_insertInserts element into queue in front of specified element QUE_removeRemoves specified element from queue QUE_new…. QUE APIDescription QUE_createCreate a queue QUE_deleteDelete a queue Mod 10 22 T TO Technical Training Organization
23
“Synchronous QUE” Option Talker QUE_put(&myQ,msg); SEM_post(&myQSem); Listener SEM_pend(&myQSem,-1); msg=QUE_get(&myQ); 23 T TO Technical Training Organization
24
Inter-Thread Comm & Synch + any number of messages can be passed + message can be anything desired (beginning with MSGQ_header) + message notification is user specified (e.g. semaphore, polling, etc.) + can be used between all thread types with no API adaptation + API unchanged even when going trans-processor ! Overview ATMAtomic Fxns SEMSemaphore LCKLock MBXMailbox QUEQueue MSGQMessage Queue 24 T TO Technical Training Organization
25
listener thread MSGQ “myQ” MSGQ Concepts (1/4) MSGQ transactions begin with listener opening a MSGQ Listener’s attempt to get a message results in a block (when semaphore specified), since no messages are in the queue yet MSGQ_open( “myQ”, &hQ,... ); MSGQ_get( hQ, &msg,... ); 25 T TO Technical Training Organization
26
MSGQ_locate(“myQ”, &hQ,.. ); MSGQ_alloc( poolid, &msg,.. ); msg->myMsg = …; MSGQ_put( msg, hQ ); talker thread POOL listener thread MSGQ_Header myMsg MyMsgqMsg MSGQ “myQ” MSGQ Concepts (2/4) Talker begins by locating the MSGQ opened by the listener Talker gets a message block from a pool and fills it as desired Talker puts the message into the MSGQ MSGQ_open( “myQ”, &hQ,... ); MSGQ_get( hQ, &msg,... ); typedef struct MyMsgqMsg { MSGQ_MsgHeader header;... } MyMsg; 26 T TO Technical Training Organization
27
MSGQ_locate(“myQ”, &hQ,.. ); MSGQ_alloc( poolid, &msg,.. ); msg->myMsg = …; MSGQ_put( msg, hQ ); MSGQ_open( “myQ”, &hQ,... ); MSGQ_get( hQ, &msg,... ); eval( msg->myMsg ); MSGQ_free( msg ); talker thread POOL listener thread MSGQ_Header myMsg MyMsgqMsg MSGQ “myQ” MSGQ Concepts (3/4) Once talker puts message to MSGQ, listener is unblocked Listener can now read/evaluate received message Listener frees message back to pool 27 T TO Technical Training Organization
28
MSGQ_locate(“myQ”, &hQ,.. ); MSGQ_alloc( poolid, &msg,.. ); msg->myMsg = …; MSGQ_put( msg, hQ ); ` MSGQ_open( “myQ”, &hQ,... ); MSGQ_get( hQ, &msg,... ); eval( msg->myMsg ); MSGQ_free( msg ); talker thread POOL listener thread MSGQ_Header myMsg MyMsgqMsg MSGQ “myQ” MSGQ Concepts (4/4) The message object manages queuing of messages passed An allocator mechanism is for getting buffers; standard = POOL A transport mechanism can be specified for trans-processor MSGQ MSGQ APIs Transports POOL0 POOLN... Msg Pool 28 T TO Technical Training Organization
29
MSGQ_locate(“myQ”, &hQ,.. ); MSGQ_alloc( poolid, &msg,.. ); msg->myMsg = …; MSGQ_put( msg, hQ ); ` MSGQ_open( “myQ”, &hQ,... ); MSGQ_get( hQ, &msg,... ); eval( msg->myMsg ); MSGQ_free( msg ); Multiprocessor MSGQ MSGQ “ myQ ” MQT MQTIOM IOM MQTIOM IOM POOL MSGQ_locate doesn’t find “myQ” locally MSGQ Transport (MQT) finds myQ on Proc1 MSGQ_put sends block to MQT MQT sends data over physical link it manages free of buffer back to local pool is implemented by MQT Listener TSK has no knowledge of location of talker Thread code is unchanged from local processor solution ! SRIO, LINK versions available talker thread listener thread 29
30
MSGQ API MSGQ_locate MSGQ_open MSGQ_put MSGQ_get MSGQ_free MSGQ_close once per object (either *) ongoing... once (or never) per object writerreaderany MSGQ_alloc MSGQ_release MSGQ_getMsgSize() Return the message size from a message MSGQ_count() Return number of messages in a message queue MSGQ_getMsgId()Return the message ID from a message MSGQ_getDstQueue()Get destination message queue MSGQ_getSrcQueue()Extract the reply destination from a message MSGQ_locateAsync()Asynchronously find a message queue (by writer) MSGQ_isLocalQueue()Returns TRUE if local message queue MSGQ_getAttrs()Returns the attributes of a local message queue MSGQ_setMsgId()Sets the message ID in a message MSGQ_setSrcQueue()Sets the reply destination in a message MSGQ_setErrorHandler()Set up handling of internal MSGQ errors 30 T TO Technical Training Organization
31
MSGQ_MsgHeader typedef struct MSGQ_MsgHeader { Uint32 reserved[2]; // Transport specific Uint16 srcProcId; // Proc Id for the src message queue Uint16 poolId; // Id of the allocator that allocated the msg Uint16 size; // Size of the allocated msg Uint16 dstId; // Destinaton message queue id Uint16 srcId; // Source message queue id Uint16 msgId; // User specified message id } MSGQ_MsgHeader, *MSGQ_Msg; 31 T TO Technical Training Organization
32
MSGQ Notification – Flexible Interface MSGQ_Attrs attrs; // TSK MSGQ_Queue hQ; attrs.post = (MSGQ_Post) SEM_postBinary; attrs.pend = (MSGQ_Pend) SEM_pendBinary; attrs.notifyHandle= (Ptr) hMySem; MSGQ_open (“myQ”, &hQ, &attrs); typedef struct MSGQ_Attrs { PtrnotifyHandle Bool(* pend)(Ptr notifyHandle, Uns timeout) // called in MSGQ_get Void(* post)(Ptr notifyHandle)// called on MSGQ_put } MSGQ_Attrs MSGQ_get() and MSGQ_put() don’t specify a signalling mechanism Based on listener thread type, user selects how to signal via MSGQ_open() TSK signalled by SEM_post, SWI by SWI_post. Default is no signal (poll) MSGQ_Attrs structure defines these options as argument for MSGQ_open() // SWI SWI_post ; SYS_zero; hMySwi; // poll (default) FXN_F_nop; SYS_zero; Null; 32 T TO Technical Training Organization
33
MSGQ Setup MSGQ config allows user to select: Number of MSGQ objects present in system Number of transport mechanisms & processors present Setup of MSGQ implementation procedure: Enable MSGQ in the config file : bios.MSGQ.ENABLEMSGQ = true; Copy and paste the example code on following slide into your C file Adjust the number of MSGQ objects via NUMMSGQS Adjust the number of transports via NUMPROCS Initialize transport objects as per TI provided examples Initialize MSGQ manager referencing above two object arrays 33 T TO Technical Training Organization
34
Example Code : MSGQ Setup initFxn * fxns params object procId name queue notifyHandle pend post status MSGQ_Obj msgQueues[NUMMSGQS]; MSGQ_TransportObj transports[NUMPROCS] = {MSGQ_NOTRANSPORT}; MSGQ_Config MSGQ_config = { msgQueues, // MSGQ obj list ptr transports, // transports list ptr NUMMSGQS, // # of MSGQs NUMPROCS, // # of transports 0, // 1 st MSGQ usable MSGQ_INVALIDMSGQ, // no error handler queue POOL_INVALIDID}; // allocator id for errors MSGQ Obj 0 MSGQ Obj N-1...... 34 T TO Technical Training Organization
35
typedef struct MSGQ_Config { MSGQ_Obj*msgqQueues; // Array of message queue handles MSGQ_TransportObj *transports; // Array of transports Uint16 numMsgqQueues; // Number of message queue handles Uint16 numProcessors; // Number of processors Uint16 startUninitialized; // First msgq to init MSGQ_Queue errorQueue; // Receives async transport errors Uint16 errorPoolId;// Alloc error msgs from poolId } MSGQ_Config; typedef struct MSGQ_TransportObj { MSGQ_MqtInitinitFxn; // Transport init function MSGQ_TransportFxns*fxns; // Transport interface functions Ptrparams; // Transport-specific setup parameters Ptrobject;// Transport-specific object Uint16procId;// Processor Id that mqt talks to } MSGQ_TransportObj; typedef struct MSGQ_Obj { Stringname; // Unique name of the queue QUE_Objqueue; // Actual queue repository PtrnotifyHandle; // Used in the pend/post functions MSGQ_Pendpend;// Called within MSGQ_get() MSGQ_Postpost;// Called within MSGQ_put() Unsstatus;// Has the queue been initialized } MSGQ_Obj, *MSGQ_Handle; MSGQ Configuration Structures 35
36
Pool Setup Pool abstraction allows user to select how message buffers are obtained Only version offered by TI at present is – essentially – BUF Other pool implementations can be created based on MEM or custom styles Setup for BUF implementation procedure: Enable POOL in the config file : bios.POOL.ENABLEPOOL = true; Copy and paste the example code on following slide into your C file Adjust the dimensions of the pools in the #defines Add or remove pools as required per the style shown 36 T TO Technical Training Organization
37
Pool Structures typedef struct POOL_Config { POOL_Obj *allocators; Uint16 numAllocators; } POOL_Config; typedef struct POOL_Obj { POOL_Init initFxn; // pre-main setup fxn POOL_Fxns *fxns; // alloc, free fxns Ptr params; // setup params Ptr object; // handle (rtn) } POOL_Obj, *POOL_Handle; typedef struct STATICPOOL_Params { Ptr addr; // location of pool size_t length; // total pool size size_t bufferSize; // size of a buffer } STATICPOOL_Params; Pool Obj 0 initFxn *fxns params object Pool Obj N-1...... # * Addr Length bufferSize STATIC POOL Params POOL_config STATIC POOL Params 37 T TO Technical Training Organization
38
#define NUMMSGS0 4 // Number of msgs per allocator #define MSGSIZE0 64 // must be multiple of 8 enum { // Allocator ID and number of allocators MQASTATICID0 = 0, NUMALLOCATORS }; #pragma DATA_ALIGN(staticBuf0, 8) // As required static Char staticBuf0[MSGSIZE0 * NUMMSGS0]; static MQASTATIC_Params poolParams0 = {staticBuf0, sizeof(staticBuf0), MSGSIZE0}; static STATICPOOL_Obj poolObj0 ; static POOL_Obj allocators[NUMALLOCATORS] = { {STATICPOOL_init, (POOL_Fxns *)&STATICPOOL_FXNS, &poolParams0, &poolObj0} }; POOL_Config POOL_config = {allocators, NUMALLOCATORS}; Example Code : Two Pool Setup #pragma DATA_SECTION(staticBuf0, “myRAM”) #define NUMMSGS1 8 #define MSGSIZE1 128 MQASTATICID1, #pragma DATA_ALIGN(staticBuf1, 8) static Char staticBuf1[MSGSIZE1 * NUMMSGS1]; static MQASTATIC_Params poolParams1 = {staticBuf1, sizeof(staticBuf1), MSGSIZE1};, poolObj1 {STATICPOOL_init, (POOL_Fxns *)&STATICPOOL_FXNS, &poolParams1, &poolObj1} Two 38
39
MSGQ Feature Review ❏ Supports zero-copy transfers. ❏ Can send and receive from HWIs, SWIs and TSKs. ❏ Notification mechanism is specified by application. ❏ Timeouts are allowed when receiving messages. ❏ Receiving a message is deterministic when the timeout is zero. ❏ Sending a message is non-blocking. ❏ Readers can determine the writer and reply back. ❏ Messages can reside on any message queue. ❏ Threads can be relocated to another processor with no runtime code changes. ❏ Allows QoS (quality of service) on message buffer pools. For example, using specific buffer pools for specific message queues. 39
40
ti Technical Training Organization 40
41
ITC API Comparison QUE SIO MBX MSGQ 41 T TO Technical Training Organization
42
Overview ATMAtomic Fxns SEMSemaphore LCKLock MBXMailbox QUEQueue MSGQMessage Queue Inter-Thread Comm & Synch + any number of messages can be passed + message can be anything desired (beginning with MSGQ_header) + message notification is user specified (e.g. semaphore, polling, etc.) + can be used between all thread types with no API adaptation + API unchanged even when going trans-processor ! 42 T TO Technical Training Organization
Similar presentations
© 2025 SlidePlayer.com. Inc.
All rights reserved.