0
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
电子发烧友
开通电子发烧友VIP会员 尊享10大特权
海量资料免费下载
精品直播免费看
优质内容免费畅学
课程9折专享价
创作中心
发布
  • 发文章

  • 发资料

  • 发帖

  • 提问

  • 发视频

创作活动
AD-UCOS3-SPRD

AD-UCOS3-SPRD

  • 厂商:

    AD(亚德诺)

  • 封装:

    -

  • 描述:

    PRD LIC UCOS3 RTOS CORE CCES SGL

  • 数据手册
  • 价格&库存
AD-UCOS3-SPRD 数据手册
μC/ OS-III The Real-Time Kernel User’s Manual Weston, FL 33326 TM Micriμm Press 1290 Weston Road, Suite 306 Weston, FL 33326 USA www.micrium.com Designations used by companies to distinguish their products are often claimed as trademarks. In all instances where Micriμm Press is aware of a trademark claim, the product name appears in initial capital letters, in all capital letters, or in accordance with the vendor’s capatilization preference. Readers should contact the appropriate companies for more complete information on trademarks and trademark registrations. All trademarks and registerd trademarks in this book are the property of their respective holders. Copyright © 2010 by Micriμm Press except where noted otherwise. Published by Micriμm Press. All rights reserved. Printed in the United States of America. No part of this publication may be reproduced or distributed in any form or by any means, or stored in a database or retrieval system, without the prior written permission of the publisher; with the exception that the program listings may be entered, stored, and executed in a computer system, but they may not be reproduced for publication. The programs and code examples in this book are presented for instructional value. The programs and examples have been carefully tested, but are not guaranteed to any particular purpose. The publisher does not offer any warranties and does not guarantee the accuracy, adequacy, or completeness of any information herein and is not responsible for any errors and ommissions. The publisher assumes no liability for damages resulting from the use of the information in this book or for any infringement of the intellectual property rights of third parties that would result from the use of this information. For bulk orders, please contact Micrium Press at: +1 954 217 2036 ISBN: 978-0-9823375-9-2 600-uCOS-III-Users-Manual-002 Table of Contents Preface .................................................................................................. 13 Chapter 1 1-1 1-2 1-3 1-4 1-5 1-6 1-7 1-8 1-9 1-10 1-11 Introduction .......................................................................................... 15 Foreground/Background Systems ...................................................... 16 Real-Time Kernels ................................................................................ 17 RTOS (Real-Time Operating System) .................................................. 19 μC/OS-III ............................................................................................... 19 μC/OS, μC/OS-II and μC/OS-III Features Comparison ...................... 24 How the Book is Organized ................................................................. 26 μC/Probe .............................................................................................. 26 Conventions ......................................................................................... 27 Chapter Contents ................................................................................. 28 Licensing .............................................................................................. 32 Contacting Micrium .............................................................................. 32 Chapter 2 2-1 2-2 2-3 2-4 2-5 2-6 2-7 2-8 Directories and Files ............................................................................ 33 Application Code ................................................................................. 36 CPU ....................................................................................................... 37 Board Support Package (BSP) ............................................................ 38 μC/OS-III, CPU Independent Source Code ........................................ 39 μC/OS-III, CPU Specific Source Code ................................................ 43 μC/CPU, CPU Specific Source Code .................................................. 44 μC/LIB, Portable Library Functions ..................................................... 46 Summary .............................................................................................. 47 Chapter 3 3-1 3-2 Getting Started with μC/OS-III ............................................................ 51 Single Task Application ....................................................................... 52 Multiple Tasks Application with Kernel Objects ................................. 60 3 Table of Contents Chapter 4 4-1 4-1-1 4-2 4-2-1 4-3 4-4 Critical Sections ................................................................................... 69 Disabling Interrupts .............................................................................. 70 Measuring Interrupt Disable Time ....................................................... 70 Locking the Scheduler ......................................................................... 71 Measuring Scheduler Lock Time ......................................................... 72 μC/OS-III Features with Longer Critical Sections ............................... 73 Summary .............................................................................................. 74 Chapter 5 5-1 5-2 5-3 5-4 5-5 5-5-1 5-5-2 5-6 5-6-1 5-6-2 5-6-3 5-6-4 5-6-5 5-7 Task Management ............................................................................... 75 Assigning Task Priorities ..................................................................... 84 Determining the Size of a Stack .......................................................... 86 Detecting Task Stack Overflows ......................................................... 87 Task Management Services ................................................................ 91 Task Management Internals ................................................................ 92 Task States ........................................................................................... 92 Task Control Blocks (TCBs) ................................................................. 97 Internal Tasks ..................................................................................... 106 The Idle Task (OS_IdleTask()) ............................................................ 107 The Tick Task (OS_TickTask()) .......................................................... 109 The Statistic Task (OS_StatTask()) .................................................... 116 The Timer Task (OS_TmrTask()) ........................................................ 119 The ISR Handler Task (OS_IntQTask()) ............................................. 120 Summary ............................................................................................ 121 Chapter 6 6-1 6-2 6-3 6-4 The Ready List ................................................................................... 123 Priority Levels ..................................................................................... 124 The Ready List ................................................................................... 128 Adding Tasks to the Ready List ........................................................ 131 Summary ............................................................................................ 132 Chapter 7 7-1 7-2 7-3 7-4 7-4-1 7-4-2 Scheduling .......................................................................................... 133 Preemptive Scheduling ...................................................................... 134 Scheduling Points .............................................................................. 136 Round-Robin Scheduling .................................................................. 138 Scheduling Internals .......................................................................... 141 OSSched() .......................................................................................... 142 OSIntExit() ........................................................................................... 143 4 7-4-3 7-5 OS_SchedRoundRobin() .................................................................... 144 Summary ............................................................................................ 146 Chapter 8 8-1 8-2 8-3 Context Switching .............................................................................. 147 OSCtxSw() .......................................................................................... 150 OSIntCtxSw() ...................................................................................... 153 Summary ............................................................................................ 155 Chapter 9 9-1 9-2 9-3 9-4 9-5 9-6 9-6-1 9-6-2 9-7 9-8 9-9 Interrupt Management ....................................................................... 157 Handling CPU Interrupts .................................................................... 158 Typical μC/OS-III Interrupt Service Routine (ISR) ............................. 159 Short Interrupt Service Routine (ISR) ................................................ 162 All Interrupts Vector to a Common Location .................................... 163 Every Interrupt Vectors to a Unique Location .................................. 165 Direct and Deferred Post Methods ................................................... 166 Direct Post Method ............................................................................ 166 Deferred Post Method ....................................................................... 169 Direct vs. Deferred Post Method ....................................................... 172 The Clock Tick (or System Tick) ........................................................ 173 Summary ............................................................................................ 175 Chapter 10 10-1 Pend Lists (or Wait Lists) ................................................................... 177 Summary ............................................................................................ 182 Chapter 11 11-1 11-2 11-3 11-4 11-5 11-6 Time Management ............................................................................. 183 OSTimeDly() ........................................................................................ 184 OSTimeDlyHMSM() ............................................................................ 189 OSTimeDlyResume() .......................................................................... 191 OSTimeSet() and OSTimeGet() .......................................................... 192 OSTimeTick() ...................................................................................... 192 Summary ............................................................................................ 192 Chapter 12 12-1 12-2 12-3 Timer Management ............................................................................ 193 One-Shot Timers ................................................................................ 195 Periodic (no initial delay) .................................................................... 196 Periodic (with initial delay) ................................................................. 196 5 Table of Contents 12-4 12-4-1 12-4-2 12-4-3 12-4-4 12-5 Timer Management Internals ............................................................. 197 Timer Management Internals - Timers States .................................. 197 Timer Management Internals - OS_TMR ........................................... 198 Timer Management Internals - Timer Task ....................................... 200 Timer Management Internals - Timer List ......................................... 203 Summary ............................................................................................ 208 Chapter 13 13-1 13-2 13-3 13-3-1 13-3-2 13-3-3 13-3-4 13-3-5 13-4 13-4-1 13-5 13-6 13-7 Resource Management ...................................................................... 209 Disable/Enable Interrupts .................................................................. 212 Lock/Unlock ....................................................................................... 214 Semaphores ....................................................................................... 215 Binary Semaphores ............................................................................ 217 Counting Semaphores ....................................................................... 224 Notes on Semaphores ....................................................................... 226 Semaphore Internals (for resource sharing) ..................................... 227 Priority Inversions .............................................................................. 232 Mutual Exclusion Semaphores (Mutex) ............................................ 234 Mutual Exclusion Semaphore Internals ............................................ 239 Should You Use a Semaphore Instead of a Mutex? ........................ 245 Deadlocks (or Deadly Embrace) ........................................................ 245 Summary ............................................................................................ 250 Chapter 14 14-1 14-1-1 14-1-2 14-1-3 14-1-4 14-2 14-2-1 14-2-2 14-2-3 14-3 14-3-1 14-3-2 14-4 14-5 Synchronization ................................................................................. 251 Semaphores ....................................................................................... 252 Unilateral Rendezvous ....................................................................... 254 Credit Tracking ................................................................................... 257 Multiple Tasks Waiting on a Semaphore .......................................... 259 Semaphore Internals (for synchronization) ....................................... 260 Task Semaphore ................................................................................ 267 Pending (i.e., Waiting) on a Task Semaphore ................................... 268 Posting (i.e., Signaling) a Task Semaphore ...................................... 269 Bilateral Rendezvous ......................................................................... 271 Event Flags ......................................................................................... 273 Using Event Flags .............................................................................. 275 Event Flags Internals ......................................................................... 279 Synchronizing Multiple Tasks ............................................................ 286 Summary ............................................................................................ 288 6 Chapter 15 15-1 15-2 15-3 15-4 15-5 15-6 15-7 15-8 15-9 15-10 Message Passing ............................................................................... 289 Messages ........................................................................................... 290 Message Queues ............................................................................... 290 Task Message Queue ........................................................................ 292 Bilateral Rendezvous ......................................................................... 293 Flow Control ....................................................................................... 294 Keeping the Data in Scope ................................................................ 296 Using Message Queues ..................................................................... 299 Clients and Servers ............................................................................ 307 Message Queues Internals ................................................................ 308 Summary ............................................................................................ 311 Chapter 16 16-1 Pending On Multiple Objects ............................................................. 313 Summary ............................................................................................ 321 Chapter 17 17-1 17-2 17-3 17-4 17-5 Memory Management ........................................................................ 323 Creating a Memory Partition ............................................................. 324 Getting a Memory Block from a Partition ......................................... 328 Returning a Memory Block to a Partition .......................................... 329 Using Memory Partitions ................................................................... 330 Summary ............................................................................................ 333 Chapter 18 18-1 18-2 18-3 18-4 Porting μC/OS-III ................................................................................ 335 μC/CPU ............................................................................................... 338 μC/OS-III Port ..................................................................................... 341 Board Support Package (BSP) .......................................................... 343 Summary ............................................................................................ 345 Chapter 19 19-1 19-2 19-3 19-4 19-5 19-6 Run-Time Statistics ............................................................................ 347 General Statistics – Run-Time ........................................................... 348 Per-Task Statistics – Run-Time ......................................................... 352 Kernel Object – Run-Time .................................................................. 355 OS_DBG.C – Static ............................................................................ 358 OS_CFG_APP.C – Static .................................................................... 371 Summary ............................................................................................ 373 7 Table of Contents Appendix A A-1 A-2 A-3 A-4 A-5 A-6 A-7 A-8 A-9 A-10 A-11 8 μC/OS-III API Reference Manual ....................................................... 375 Task Management ............................................................................. 376 Time Management ............................................................................. 378 Mutual Exclusion Semaphores – Resource Management ............... 379 Event Flags – Synchronization .......................................................... 380 Semaphores – Synchronization ......................................................... 381 Task Semaphores – Synchronization ................................................ 382 Message Queues – Message Passing .............................................. 383 Task Message Queues – Message Passing ..................................... 384 Pending on Multiple Objects ............................................................. 385 Timers ................................................................................................. 386 Fixed-Size Memory Partitions – Memory Management ................... 387 OSCtxSw() ........................................................................................... 388 OSFlagCreate() .................................................................................... 390 OSFlagDel() ......................................................................................... 392 OSFlagPend() ...................................................................................... 394 OSFlagPendAbort() ............................................................................. 398 OSFlagPendGetFlagsRdy()................................................................. 401 OSFlagPost() ....................................................................................... 403 OSIdleTaskHook() ............................................................................... 405 OSInit() ................................................................................................. 407 OSInitHook() ........................................................................................ 409 OSIntCtxSw() ....................................................................................... 410 OSIntEnter() ......................................................................................... 412 OSIntExit()............................................................................................ 413 OSMemCreate()................................................................................... 414 OSMemGet()........................................................................................ 417 OSMemPut() ........................................................................................ 419 OSMutexCreate()................................................................................. 421 OSMutexDel() ...................................................................................... 423 OSMutexPend() ................................................................................... 425 OSMutexPendAbort().......................................................................... 428 OSMutexPost() .................................................................................... 430 OSPendMulti() ..................................................................................... 432 OSQCreate() ........................................................................................ 436 OSQDel() .............................................................................................. 438 OSQFlush() .......................................................................................... 440 OSQPend()........................................................................................... 442 OSQPendAbort() ................................................................................. 446 OSQPost()............................................................................................ 448 OSSafetyCriticalStart() ........................................................................ 451 OSSched() ........................................................................................... 452 OSSchedLock() ................................................................................... 454 OSSchedRoundRobinCfg() ................................................................. 456 OSSchedRoundRobinYield() .............................................................. 458 OSSchedUnlock() ................................................................................ 460 OSSemCreate().................................................................................... 462 OSSemDel() ......................................................................................... 464 OSSemPend() ...................................................................................... 467 OSSemPendAbort()............................................................................. 470 OSSemPost() ....................................................................................... 472 OSSemSet() ......................................................................................... 475 OSStart() .............................................................................................. 477 OSStartHighRdy()................................................................................ 479 OSStatReset()...................................................................................... 481 OSStatTaskCPUUsageInit() ................................................................ 482 OSStatTaskHook()............................................................................... 483 OSTaskChangePrio()........................................................................... 485 OSTaskCreate() ................................................................................... 487 OSTaskCreateHook() .......................................................................... 498 OSTaskDel()......................................................................................... 500 OSTaskDelHook() ................................................................................ 502 OSTaskQPend()................................................................................... 504 OSTaskQPendAbort().......................................................................... 507 OSTaskQPost().................................................................................... 509 OSTaskRegGet().................................................................................. 512 OSTaskRegSet() .................................................................................. 514 OSTaskReturnHook() .......................................................................... 516 OSTaskResume()................................................................................. 518 OSTaskSemPend() .............................................................................. 520 OSTaskSemPendAbort()..................................................................... 523 OSTaskSemPost() ............................................................................... 525 OSTaskSemSet() ................................................................................. 527 OSTaskStatHook()............................................................................... 529 OSTaskStkChk() .................................................................................. 531 OSTaskStkInit().................................................................................... 534 9 Table of Contents OSTaskSuspend() ............................................................................... 538 OSTaskSwHook() ................................................................................ 540 OSTaskTimeQuantaSet() .................................................................... 543 OSTickISR() ......................................................................................... 545 OSTimeDly()......................................................................................... 547 OSTimeDlyHMSM() ............................................................................. 550 OSTimeDlyResume() ........................................................................... 553 OSTimeGet() ........................................................................................ 555 OSTimeSet() ........................................................................................ 556 OSTimeTick() ....................................................................................... 558 OSTimeTickHook() .............................................................................. 559 OSTmrCreate() .................................................................................... 561 OSTmrDel() .......................................................................................... 567 OSTmrRemainGet() ............................................................................. 569 OSTmrStart()........................................................................................ 571 OSTmrStateGet() ................................................................................. 573 OSTmrStop()........................................................................................ 575 OSVersion().......................................................................................... 577 Appendix B B-1 B-2 B-3 μC/OS-III Configuration Manual ........................................................ 579 μC/OS-III Features (OS_CFG.H) ........................................................ 583 Data Types (OS_TYPE.H) ................................................................... 593 μC/OS-III Stacks, Pools and other (OS_CFG_APP.H) ...................... 594 Appendix C C-1 C-2 C-3 C-4 C-4-1 C-4-2 C-4-3 C-4-4 C-4-5 C-4-6 C-4-7 C-4-8 Migrating from μC/OS-II to μC/OS-III ................................................ 599 Differences in Source File Names and Contents .............................. 602 Convention Changes ......................................................................... 605 Variable Name Changes .................................................................... 611 API Changes ....................................................................................... 612 Event Flags ......................................................................................... 613 Message Mailboxes ........................................................................... 615 Memory Management ........................................................................ 617 Mutual Exclusion Semaphores .......................................................... 618 Message Queues ............................................................................... 620 Semaphores ....................................................................................... 622 Task Management ............................................................................. 624 Time Management ............................................................................. 628 10 C-4-9 C-4-10 C-4-11 Timer Management ............................................................................ 629 Miscellaneous .................................................................................... 631 Hooks and Port .................................................................................. 633 Appendix D D-1 D-2 D-3 D-4 D-5 MISRA-C:2004 and μC/OS-III ............................................................ 637 MISRA-C:2004, Rule 8.5 (Required) .................................................. 638 MISRA-C:2004, Rule 8.12 (Required) ................................................ 638 MISRA-C:2004, Rule 14.7 (Required) ................................................ 639 MISRA-C:2004, Rule 15.2 (Required) ................................................ 640 MISRA-C:2004, Rule 17.4 (Required) ................................................ 641 Appendix E Bibliography ....................................................................................... 643 Appendix F Licensing Policy ................................................................................. 645 Index ............................................................................................................................. 647 11 Table of Contents 12 Preface WHAT IS μC/OS-III? μC/OS-III (pronounced “Micro C O S Three) is a scalable, ROMable, preemptive real-time kernel that manages an unlimited number of tasks. μC/OS-III is a third-generation kernel and offers all of the services expected from a modern real-time kernel, such as resource management, synchronization, inter-task communications, and more. However, μC/OS-III offers many unique features not found in other real-time kernels, such as the ability to complete performance measurements at run-time, to directly signal or send messages to tasks, achieve pending on multiple kernel objects, and more. WHY A NEW μC/OS VERSION? The μC/OS series, first introduced in 1992, has undergone a number of changes over the years based on feedback from thousands of people using and deploying its evolving versions. μC/OS-III is the sum of this feedback and experience. Rarely used μC/OS-II features were eliminated and newer, more efficient features and services, were added. Probably the most common request was to add round robin scheduling, which was not possible for μC/OS-II, but is now a feature of μC/OS-III. μC/OS-III also provides additional features that better exploit the capabilities of today’s newer processors. Specifically, μC/OS-III was designed with 32-bit processors in mind, although it certainly works well with 16- and even several 8-bit processors. 13 Preface μC/OS-III GOALS The main goal of μC/OS-III is to provide a best-in-class real-time kernel that literally shaves months of development time from an embedded-product schedule. Using a commercial real-time kernel such as μC/OS-III provides a solid foundation and framework to the design engineer dealing with the growing complexity of embedded designs. Another goal for μC/OS-III, and therefore this book, is to explain inner workings of a commercial-grade kernel. This understanding will assist the reader in making logical design decisions and informed tradeoffs between hardware and software that make sense. 14 Chapter 1 Introduction Real-time systems are systems whereby the correctness of the computed values and their timeliness are at the forefront. There are two types of real-time systems, hard and soft real time. What differentiates hard and soft real-time systems is their tolerance to missing deadlines and the consequences associated with those misses. Correctly computed values after a deadline has passed are often useless. For hard real-time systems, missing deadlines is not an option. In fact, in many cases, missing a deadline often results in catastrophe, which may involve human lives. For soft real-time systems, however, missing deadlines is generally not as critical. Real-time applications cover a wide range, but many real-time systems are embedded. An embedded system is a computer built into a system and not acknowledged by the user as being a computer. The following list shows just a few examples of embedded systems: Aerospace Communications Process control ■ Flight management systems ■ Jet engine controls ■ Weapons systems ■ Routers ■ Switches ■ Cell phones ■ Chemical plants ■ Factory automation ■ Food processing Audio Computer peripherals Robots ■ MP3 players ■ Amplifiers and tuners ■ Printers ■ Scanners Video Automotive Domestic ■ ■ ■ ■ ■ Air conditioning units ■ Thermostats ■ White goods Antilock braking systems Climate control Engine controls Navigation systems (GPS) ■ Broadcasting equipment ■ HD Televisions And many more Office automation ■ FAX machines / copiers Real-time systems are typically more complicated to design, debug, and deploy than non-real-time systems. 15 Chapter 1 1-1 FOREGROUND/BACKGROUND SYSTEMS Small systems of low complexity are typically designed as foreground/background systems or super-loops. An application consists of an infinite loop that calls modules (i.e., tasks) to perform the desired operations (background). Interrupt Service Routines (ISRs) handle asynchronous events (foreground). Foreground is also called interrupt level; background is called task level. Critical operations that should be performed at the task level must unfortunately be handled by the ISRs to ensure that they are dealt with in a timely fashion. This causes ISRs to take longer than they should. Also, information for a background module that an ISR makes available is not processed until the background routine gets its turn to execute, which is called the task-level response. The worst-case task-level response time depends on how long a background loop takes to execute since the execution time of typical code is not constant, the time for successive passes through a portion of the loop is nondeterministic. Furthermore, if a code change is made, the timing of the loop is affected. Most high-volume and low-cost microcontroller-based applications (e.g., microwave ovens, telephones, toys, etc.) are designed as foreground/background systems. 6XSHU/RRS %DFNJURXQG 7DVN 7DVN 7DVN 7LPH ,65 )RUHJURXQG ,65 ,QILQLWH /RRS 7DVN 1HVWHG ,65 7DVN ,65 )RUHJURXQG ,65 ,65 7DVN Figure 1-1 Foreground/Background (SuperLoops) systems 16 Introduction 1-2 REAL-TIME KERNELS A real-time kernel is software that manages the time and resources of a microprocessor, microcontroller or Digital Signal Processor (DSP). The design process of a real-time application involves splitting the work into tasks, each responsible for a portion of the job. A task (also called a thread) is a simple program that thinks it has the Central Processing Unit (CPU) completely to itself. On a single CPU, only one task executes at any given time. The kernel is responsible for the management of tasks. This is called multitasking. Multitasking is the process of scheduling and switching the CPU between several tasks. The CPU switches its attention between several sequential tasks. Multitasking provides the illusion of having multiple CPUs and maximizes the use of the CPU. Multitasking also helps in the creation of modular applications. One of the most important aspects of multitasking is that it allows the application programmer to manage the complexity inherent in real-time applications. Application programs are easier to design and maintain when multitasking is used. μC/OS-III is a preemptive kernel, which means that μC/OS-III always runs the most important task that is ready to run as shown in Figure 1-2. :DLWIRU(YHQW /RZ3ULRULW\ 7DVN   (YHQWWKDW +LJK3ULRULW\7DVN LV:DLWLQJIRU 7LPH :DLWIRU(YHQW  ,QILQLWH /RRS +LJK3ULRULW\ 7DVN  ,65  ,QILQLWH /RRS  /RZ3ULRULW\ 7DVN  Figure 1-2 μC/OS-III is a preemptive kernel 17 Chapter 1 F1-2(1) A low-priority task is executing. F1-2(2) An interrupt occurs, and the CPU vectors to the ISR responsible for servicing the interrupting device. F1-2(3) The ISR services the interrupt device, but actually does very little work. The ISR will signal or send a message to a higher-priority task that will be responsible for most of the processing of the interrupting device. For example, if the interrupt comes from an Ethernet controller, the ISR simply signals a task, which will process the received packet. F1-2(4) When the ISR finishes, μC/OS-III notices that a more important task has been made ready to run by the ISR and will not return to the interrupted task, but instead context switch to the more important task. F1-2(5) The higher-priority task executes and performs the necessary processing in response to the interrupt device. F1-2(6) When the higher-priority task completes its work, it loops back to the beginning of the task code and makes a μC/OS-III function call to wait for the next interrupt from the device. F1-2(7) The low-priority task resumes exactly at the point where it was interrupted, not knowing what happened. Kernels such as μC/OS-III are also responsible for managing communication between tasks, and managing system resources (memory and I/O devices). A kernel adds overhead to a system because the services provided by the kernel require time to execute. The amount of overhead depends on how often these services are invoked. In a well-designed application, a kernel uses between 2% and 4% of a CPU’s time. And, since μC/OS-III is software that is added to an application, it requires extra ROM (code space) and RAM (data space). Low-end single-chip microcontrollers are generally not able to run a real-time kernel such as μC/OS-III since they have access to very little RAM. μC/OS-III requires between 1 Kbyte and 4 Kbytes of RAM, plus each task requires its own stack space. It is possible for μC/OS-III to work on processors having as little as 4 Kbytes of RAM. 18 Introduction Finally, μC/OS-III allows for better use of the CPU by providing approximately 70 indispensable services. After designing a system using a real-time kernel such as μC/OS-III, you will not return to designing a foreground/background system. 1-3 RTOS (REAL-TIME OPERATING SYSTEM) A Real Time Operating System generally contains a real-time kernel and other higher-level services such as file management, protocol stacks, a Graphical User Interface (GUI), and other components. Most additional services revolve around I/O devices. Micriμm offers a complete suite of RTOS components including: μC/FS (an Embedded File System), μC/TCP-IP (a TCP/IP stack), μC/GUI (a Graphical User Interface), μC/USB (a USB device, host and OTG stack), and more. Most of these components are designed to work standalone. Except for μC/TCP-IP, a real-time kernel is not required to use the components in an application. In fact, users can pick and choose only the components required for the application. Contact Micriμm (www.micrium.com) for additional details and pricing. 1-4 μC/OS-III μC/OS-III is a scalable, ROMable, preemptive real-time kernel that manages an unlimited number of tasks. μC/OS-III is a third-generation kernel, offering all of the services expected from a modern real-time kernel including resource management, synchronization, inter-task communication, and more. However, μC/OS-III also offers many unique features not found in other real-time kernels, such as the ability to perform performance measurements at run time, directly signal or send messages to tasks, and pending (i.e., waiting) on such multiple kernel objects as semaphores and message queues. Here is a list of features provided by μC/OS-III: Source Code: μC/OS-III is provided in ANSI-C source form to licensees. The source code for μC/OS-III is arguably the cleanest and most consistent kernel code available. Clean source is part of the corporate culture at Micriμm. Although many commercial kernel vendors provide source code for their products, unless the code follows strict coding standards and is accompanied by complete documentation with examples to show how the 19 Chapter 1 code works, these products may be cumbersome and difficult to harness. With this book, you will gain a deep understanding of the inner workings of μC/OS-III, which will protect your investment. Intuitive Application Programming Interface (API): μC/OS-III is highly intuitive. Once familiar with the consistent coding conventions used, it is simple to predict the functions to call for the services required, and even predict which arguments are needed. For example, a pointer to an object is always the first argument, and a pointer to an error code is always the last one. Preemptive multitasking: μC/OS-III is a preemptive multi-tasking kernel and therefore, μC/OS-III always runs the most important ready-to-run task. Round robin scheduling of tasks at equal priority: μC/OS-III allows multiple tasks to run at the same priority level. When multiple tasks at the same priority are ready to run, and that priority level is the most important level, μC/OS-III runs each task for a user-specified time called a time quanta. Each task can define its own time quanta, and a task can also give up the CPU to another task at the same priority if it does not require the full time quanta. Low interrupt disable time: μC/OS-III has a number of internal data structures and variables that it needs to access atomically. To ensure this, μC/OS-III is able to protect these critical regions by locking the scheduler instead of disabling interrupts. Interrupts are therefore disabled for very little time. This ensures that μC/OS-III is able to respond to some of the fastest interrupt sources. Deterministic: Interrupt response with μC/OS-III is deterministic. Also, execution times of most services provided by μC/OS-III are deterministic. Scalable: The footprint (both code and data) can be adjusted based on the requirements of the application. This assumes access to the source code for μC/OS-III since adding and removing features (i.e., services) is performed at compile time through approximately 40 #defines (see OS_CFG.H). μC/OS-III also performs a number of run-time checks on arguments passed to μC/OS-III services. Specifically, μC/OS-III verifies that the user is not passing NULL pointers, not calling task level services from ISRs, that arguments are within allowable range, and options specified are valid, etc.. These checks can be disabled (at compile time) to further reduce the code footprint and improve performance. The fact that μC/OS-III is scalable allows it to be used in a wide range of applications and projects. 20 Introduction Portable: μC/OS-III can be ported to a large number of CPU architectures. Most μC/OS-II ports are easily converted to work on μC/OS-III with minimal changes in just a matter of minutes and therefore benefit from more than 45 CPU architectures already supported by μC/OS-II. ROMable: μC/OS-III was designed especially for embedded systems and can be ROMed along with the application code. Run-time configurable: μC/OS-III allows the user to configure the kernel at run time. Specifically, all kernel objects such as tasks, stacks, semaphores, event-flag groups, message queues, number of messages, mutual exclusion semaphores, memory partitions and timers, are allocated by the user at run time. This prevents over-allocating resources at compile time. Unlimited number of tasks: μC/OS-III supports an unlimited number of tasks. From a practical standpoint, however, the number of tasks is actually limited by the amount of memory (both code and data space) that the processor has access to. Each task requires its own stack space and, μC/OS-III provides features to allow stack growth of the tasks to be monitored at run-time. μC/OS-III does not impose any limitations on the size of each task, except that there be a minimum size based on the CPU used. Unlimited number of priorities: μC/OS-III supports an unlimited number of priority levels. However, configuring μC/OS-III for between 32 and 256 different priority levels is more than adequate for most applications. Unlimited number of kernel objects: μC/OS-III allows for any number of tasks, semaphores, mutual exclusion semaphores, event flags, message queues, timers, and memory partitions. The user at run-time allocates all kernel objects. Services: μC/OS-III provides all the services expected from a high-end real-time kernel, such as task management, time management, semaphores, event flags, mutexes, message queues, software timers, fixed-size memory pools, etc. Mutual Exclusion Semaphores (Mutexes): Mutexes are provided for resource management. Mutexes are special types of semaphores that have built-in priority inheritance, which eliminate unbounded priority inversions. Accesses to a mutex can be nested and therefore, a task can acquire the same mutex up to 250 times. Of course, the mutex owner needs to release the mutex an equal number of times. 21 Chapter 1 Nested task suspension: μC/OS-III allows a task to suspend itself or another task. Suspending a task means that the task will not be allowed to execute until the task is resumed by another task. Suspension can be nested up to 250 levels deep. In other words, a task can suspend another task up to 250 times. Of course, the task must be resumed an equal number of times for it to become eligible to run on the CPU. Software timers: Define any number of “one-shot” and/or “periodic” timers. Timers are countdown counters that perform a user-definable action upon counting down to 0. Each timer can have its own action and, if a timer is periodic, the timer is automatically reloaded and the action is executed every time the countdown reaches zero. Pend on multiple objects: μC/OS-III allows an application to wait (i.e., pend) on multiple events at the same time. Specifically, a task can wait on multiple semaphores and/or message queues to be posted. The waiting task wakes up as soon as one of the events occurs. Task Signals: μC/OS-III allows an ISR or task to directly signal a task. This avoids having to create an intermediate kernel object such as a semaphore or event flag just to signal a task, and results in better performance. Task Messages: μC/OS-III allows an ISR or a task to send messages directly to a task. This avoids having to create and use a message queue, and also results in better performance. Task registers: Each task can have a user-definable number of “task registers.” Task registers are different than CPU registers. Task registers can be used to hold “errno” type variable, IDs, interrupt disable time measurement on a per-task basis, and more. Error checking: μC/OS-III verifies that NULL pointers are not passed, that the user is not calling task-level services from ISRs, that arguments are within allowable range, that options specified are valid, that a handler is passed to the proper object as part of the arguments to services that manipulate the desired object, and more. Each μC/OS-III API function returns an error code concerning the outcome of the function call. Built-in performance measurements: μC/OS-III has built-in features to measure the execution time of each task, stack usage of each task, number of times a task executes, CPU usage, ISR-to-task and task-to-task response time, peak number of entries in certain lists, interrupt disable and scheduler lock time on a per-task basis, and more. 22 Introduction Can easily be optimized: μC/OS-III was designed so that it could easily be optimized based on the CPU architecture. Most data types used in μC/OS-III can be changed to make better use of the CPU’s natural word size. Also, the priority resolution algorithm can easily be written in assembly language to benefit from special instructions such as bit set and clear, as well as count-leading-zeros (CLZ), or find-first-one (FF1) instructions. Deadlock prevention: All of the μC/OS-III “pend” services include timeouts, which help avoid deadlocks. Tick handling at task level: The clock tick manager in μC/OS-III is accomplished by a task that receives a trigger from an ISR. Handling delays and timeouts by a task greatly reduces interrupt latency. Also, μC/OS-III uses a hashed delta list mechanism, which further reduces the amount of overhead in processing delays and timeouts of tasks. User definable hooks: μC/OS-III allows the port and application programmer to define “hook” functions, which are called by μC/OS-III. A hook is simply a defined function that allows the user to extend the functionality of μC/OS-III. One such hook is called during a context switch, another when a task is created, yet another when a task is deleted, etc. Timestamps: For time measurements, μC/OS-III requires that a 16-bit or 32-bit free running counter be made available. This counter can be read at run time to make time measurements of certain events. For example, when an ISR posts a message to a task, the timestamp counter is automatically read and saved as part of the message posted. When the recipient receives the message, the timestamp is provided to the recipient, and by reading the current timestamp, the time it took for the message to be received can be determined. Built-in support for Kernel Awareness debuggers: This feature allows kernel awareness debuggers to examine and display μC/OS-III variables and data structures in a user-friendly way, but only when the debugger hits a breakpoint. Instead of a static view of the environment the kernel awareness support in μC/OS-III is also used by μC/Probe to display the same information at run-time. Object names: Each μC/OS-III kernel object can have a name associated with it. This makes it easy to recognize what the object is assigned to. Assign an ASCII name to a task, a semaphore, a mutex, an event flag group, a message queue, a memory partition, and a timer. The object name can have any length, but must be NUL terminated. 23 Chapter 1 1-5 μC/OS, μC/OS-II AND μC/OS-III FEATURES COMPARISON Table 1-1 shows the evolution of μC/OS over the years, comparing the features available in each version. Feature μC/OS μC/OS-II μC/OS-III 1992 1998 2009 Book Yes Yes Yes Source code available Yes Yes Year introduced Yes (Licensees only) Preemptive Multitasking Yes Yes Yes Maximum number of tasks 64 255 Unlimited Number of tasks at each priority level 1 1 Unlimited Round Robin Scheduling No No Yes Semaphores Yes Yes Yes Mutual Exclusion Semaphores No Yes Yes (Nestable) Event Flags No Yes Yes Message Mailboxes Yes Yes No (not needed) Message Queues Yes Yes Yes Fixed Sized Memory Management No Yes Yes Signal a task without requiring a semaphore No No Yes Send messages to a task without requiring a message queue No No Yes Software Timers No Yes Yes Task suspend/resume No Yes Yes (Nestable) Deadlock prevention Yes Yes Yes Scalable Yes Yes Yes Code Footprint 3K to 8K 6K to 26K 6K to 20K Data Footprint 1K+ 1K+ 1K+ Yes Yes Yes ROMable 24 Introduction Feature μC/OS μC/OS-II μC/OS-III Run-time configurable No No Yes Compile-time configurable Yes Yes Yes ASCII names for each kernel object No Yes Yes Pend on multiple objects No Yes Yes Task registers No Yes Yes Built-in performance measurements No Limited Extensive User definable hook functions No Yes Yes Time stamps on posts No No Yes Built-in Kernel Awareness support No Yes Yes Optimizable Scheduler in assembly language No No Yes Tick handling at task level No No Yes Source code available Yes Yes Yes Number of services ~20 ~90 ~70 MISRA-C:1998 No Yes (except 10 rules) N/A MISRA-C:2004 No No Yes (except 7 rules) DO178B Level A and EUROCAE ED-12B No Yes In progress Medical FDA pre-market notification (510(k)) and pre-market approval (PMA) No Yes In progress SIL3/SIL4 IEC for transportation and nuclear systems No Yes In progress IEC-61508 No Yes In progress Table 1-1 μC/OS-II and μC/OS-III Features Comparison Chart 25 Chapter 1 1-6 HOW THE BOOK IS ORGANIZED This book consists of two books in one. Part I describes μC/OS-III and is not tied to any specific CPU architecture. Here, the reader will learn about real-time kernels through μC/OS-III. Specifically, critical sections, task management, the ready list, scheduling, context switching, interrupt management, wait lists, time management, timers, resource management, synchronization, memory management, how to use μC/OS-III’s API, how to configure μC/OS-III, and how to port μC/OS-III to different CPU architectures, are all covered. Part II describes the port of a popular CPU architecture. Here, learn about this CPU architecture and how μC/OS-III gets the most out of the CPU. Examples are provided to actually run code on the evaluation board that is available with this book. As I just mentioned, this book assumes the presence of an evaluation board that allows the user to experiment with the wonderful world of real-time kernels, and specifically μC/OS-III. The book and board are complemented by a full set of tools that are provided free of charge either in a companion CD/DVD, or downloadable through the Internet. The tools and the use of μC/OS-III are free as long as they are used with the evaluation board, and there is no commercial intent to use them on a project. In other words, there is no additional charge except for the initial cost of the book, evaluation board and tools, as long as they are used for educational purposes. The book also comes with a trial version of an award-winning tool from Micriμm called μC/Probe. The trial version allows the user to monitor and change up to five variables in a target system. 1-7 μC/PROBE μC/Probe is a Microsoft Windows™ based application that enables the user to visualize variables in a target at run time. Specifically, display or change the value of any variable in a system while the target is running. These variables can be displayed using such graphical elements as gauges, meters, bar graphs, virtual LEDs, numeric indicators, and many more. Sliders, switches, and buttons can be used to change variables. This is accomplished without the user writing a single line of code! 26 Introduction μC/Probe interfaces to any target (8-, 16-, 32-, 64-bit, or even DSPs) through one of the many interfaces supported ( J-Tag, RS-232C, USB, Ethernet, etc.). μC/Probe displays or changes any variable (as long as they are global) in the application, including μC/OS-III’s internal variables. μC/Probe works with any compiler/assembler/linker able to generate an ELF/DWARF or IEEE695 file. This is the exact same file that the user will download to the evaluation board or a final target. From this file, μC/Probe is able to extract symbolic information about variables, and determine where variables are stored in RAM or ROM. μC/Probe also allows users to log the data displayed into a file for analysis of the collected data at a later time. μC/Probe also provides μC/OS-III kernel awareness as a built-in feature. The trial version that accompanies the book is limited to the display or change of up to five variables. μC/Probe is a tool that serious embedded software engineers should have in their toolbox. The full version of μC/Probe is included when licensing μC/OS-III. See www.micrium.com for more details. 1-8 CONVENTIONS There are a number of conventions in this book. First, notice that when a specific element in a figure is referenced, the element has a number next to it in parenthesis. A description of this element follows the figure and in this case, the letter “F” followed by the figure number, and then the number in parenthesis. For example, F3-4(2) indicates that this description refers to Figure 3-4 and the element (2) in that figure. This convention also applies to listings (starts with an “L”) and tables (starts with a “T”). Second, notice that sections and listings are started where it makes sense. Specifically, do not be surprised to see the bottom half of a page empty. New sections begin on a new page, and listings are found on a single page, instead of breaking listings on two pages. Third, code quality is something I’ve been avidly promoting throughout my whole career. At Micriμm, we pride ourselves in having the cleanest code in the industry. Examples of this are seen in this book. I created and published a coding standard in 1992 that was published 27 Chapter 1 in the original μC/OS book. This standard has evolved over the years, but the spirit of the standard has been maintained throughout. The Micriμm coding standard is available for download from the Micriμm website, www.micrium.com One of the conventions used is that all functions, variables, macros and #define constants are prefixed by “OS” (which stands for Operating System) followed by the acronym of the module (e.g., Sem), and then the operation performed by the function. For example OSSemPost() indicates that the function belongs to the OS (μC/OS-III), that it is part of the Semaphore services, and specifically that the function performs a Post (i.e., signal) operation. This allows all related functions to be grouped together in the reference manual, and makes those services intuitive to use. Notice that signaling or sending a message to a task is called posting, and waiting for a signal or a message is called pending. In other words, an ISR or a task signals or sends a message to another task by using OS???Post(), where ??? is the type of service: Sem, TaskSem, Flag, Mutex, Q, and TaskQ. Similarly, a task can wait for a signal or a message by calling OS???Pend(). 1-9 CHAPTER CONTENTS Figure 1-3 shows the layout and flow of Part I of the book. This diagram should be useful to understand the relationship between chapters. The first column on the left indicates chapters that should be read in order to understand μC/OS-III’s structure. The second column shows chapters that are related to additional services provided by μC/OS-III. The third column relates to chapters that will help port μC/OS-III to different CPU architectures. The top of the fourth column explains how to obtain valuable run-time and compile-time statistics from μC/OS-III. This is especially useful if developing a kernel awareness plug-in for a debugger, or using μC/Probe. The middle of column four contains the μC/OS-III API and configuration manuals. Reference these sections regularly when designing a product using μC/OS-III. Finally, the bottom of the last column contains miscellaneous appendices. 28 Introduction 3UHIDFH ,QWURGXFWLRQ 'LUHFWRULHV DQG )LOHV *HWWLQJ6WDUWHG ZLWK —&26,,, &ULWLFDO 6HFWLRQV 7DVN 0DQDJHPHQW 7KH 5HDG\ /LVW    7LPH 0DQDJHPHQW 7LPHU 0DQDJHPHQW 5HVRXUFH 0DQDJHPHQW      $  % 6\QFKURQL]DWLRQ  0HVVDJH 3DVVLQJ '  ,QWHUUXSW 0DQDJHPHQW  3RUWLQJ —&26,,, 0HPRU\ 0DQDJHPHQW —&26,,, &RQILJXUDWLRQ 0DQXDO —&26,,, DQG 0,65$&    3HQG /LVWV 3HQGLQJ RQ0XOWLSOH 2EMHFWV —&26,,,$3, 5HIHUHQFH 0DQXDO  6FKHGXOLQJ &RQWH[W 6ZLWFKLQJ 5XQ7LPH 6WDWLVWLFV  0LJUDWLQJIURP —&26,,WR —&26,,,  ( %LEOLRJUDSK\ & ) /LFHQVLQJ 3ROLF\ Figure 1-3 μC/OS-III Book Layout Chapter 1, Introduction. This chapter. Chapter 2, Directories and Files. This chapter explains the directory structure and files needed to build a μC/OS-III-based application. Learn about the files that are needed, where they should be placed, which module does what, and more. Chapter 3, Getting Started with μC/OS-III. In this chapter, learn how to properly initialize and start a μC/OS-III-based application. Chapter 4, Critical Sections. This chapter explains what critical sections are, and how they are protected. 29 Chapter 1 Chapter 5, Task Management. This chapter is an introduction to one of the most important aspects of a real-time kernel, the management of tasks in a multitasking environment. Chapter 6, The Ready List. In this chapter, learn how μC/OS-III efficiently keeps track of all of the tasks that are waiting to execute on the CPU. Chapter 7, Scheduling. This chapter explains the scheduling algorithms used by μC/OS-III, and how it decides which task will run next. Chapter 8, Context Switching. This chapter explains what a context switch is, and describes the process of suspending execution of a task and resuming execution of a higher-priority task. Chapter 9, Interrupt Management. Here is how μC/OS-III deals with interrupts and an overview of services that are available from Interrupt Service Routines (ISRs). Learn how μC/OS-III supports nearly any interrupt controller. Chapter 10, Pend Lists (or Wait Lists). Tasks that are not able to run are most likely blocked waiting for specific events to occur. Pend Lists (or wait lists), are used to keep track of tasks that are waiting for a resource or event. This chapter describes how μC/OS-III maintains these lists. Chapter 11, Time Management. In this chapter, learn about μC/OS-III’s services that allow users to suspend a task until some time expires. With μC/OS-III, specify to delay execution of a task for an integral number of clock ticks or until the clock-tick counter reaches a certain value. The chapter will also show how a delayed task can be resumed, and describe how to get the current value of the clock tick counter, or set this counter, if needed. Chapter 12, Timer Management. μC/OS-III allows users to define any number of software timers. When a timer expires, a function can be called to perform some action. Timers can be configured to be either periodic or one-shot. This chapter also explains how the timer-management module works. Chapter 13, Resource Management. In this chapter, learn different techniques so that tasks share resources. Each of these techniques has advantages and disadvantages that will be discussed. This chapter also explains the internals of semaphores, and mutual exclusion semaphore management. 30 Introduction Chapter 14, Synchronization. μC/OS-III provides two types of services for synchronization: semaphores and event flags and these are explained in this chapter, as well as what happens when calling specific services provided in this module. Chapter 15, Message Passing. μC/OS-III allows a task or an ISR to send messages to a task. This chapter describes some of the services provided by the message queue management module. Chapter 16, Pending on multiple objects. In this chapter, see how μC/OS-III allows an application to pend (or wait) on multiple kernel objects (semaphores or message queues) at the same time. This feature makes the waiting task ready to run as soon as any one of the objects is posted (i.e., OR condition), or a timeout occurs. Chapter 17, Memory Management. Here is how μC/OS-III’s fixed-size memory partition manager can be used to allocate and deallocate dynamic memory. Chapter 18, Porting μC/OS-III. This chapter explains, in generic terms, how to port μC/OS-III to any CPU architecture. Chapter 19, Run-Time Statistics. μC/OS-III provides a wealth of information about the run-time environment, such as number of context switches, CPU usage (as a percentage), stack usage on a per-task basis, μC/OS-III RAM usage, maximum interrupt disable time, maximum scheduler lock time, and more. Appendix A, μC/OS-III API Reference Manual. This appendix provides a alphabetical reference for all user-available services provided by μC/OS-III. Appendix B, μC/OS-III Configuration Manual. This appendix describes how to configure a μC/OS-III-based application. OS_CFG.H configures the μC/OS-III features (semaphores, queues, event flags, etc.), while OS_CFG_APP.H configures the run-time characteristics (tick rate, tick wheel size, stack size for the idle task, etc.). Appendix C, Migrating from μC/OS-II to μC/OS-III. μC/OS-III has its roots in μC/OS-II and, in fact, most of the μC/OS-II ports can be easily converted to μC/OS-III. However, most APIs have changed from μC/OS-II to μC/OS-III, and this appendix describes some of the differences. Appendix D, MISRA-C:2004 rules and μC/OS-III. μC/OS-III follows most of the MISRA-C:2004, except for 7 of these rules. 31 Chapter 1 Appendix E, Bibliography. Appendix F, Licensing μC/OS-III. 1-10 LICENSING This book contains μC/OS-III precompiled in linkable object form, an evaluation board, and tools (compiler/assembler/linker/debugger). Use μC/OS-III for free, as long as it is only used with the evaluation board that accompanies this book. You will need to purchase a license when using this code in a commercial project, where the intent is to make a profit. Users do not pay anything beyond the price of the book, evaluation board and tools, as long as they are used for educational purposes. You will need to license μC/OS-III if you intend to use μC/OS-III in a commercial product where you intend to make a profit. You need to purchase this license when you make the decision to use μC/OS-III in a design, not when you are ready to go to production. If you are unsure about whether you need to obtain a license for your application, please contact Micriμm and discuss your use with a sales representative. 1-11 CONTACTING MICRIUM Do not hesitate to contact Micriμm should you have any licensing questions regarding μC/OS-III. Micriμm 11290 Weston Road, Suite 306 Weston, FL 33326 USA Phone:  +1 954 217 2036 Fax:    +1 954 217 2037 E-mail: Licensing@Micrium.com Web:    www.Micrium.com 32 Chapter 2 Directories and Files μC/OS-III is fairly easy to use once it is understood exactly which source files are needed to make up a μC/OS-III-based application. This chapter will discuss the modules available for μC/OS-III and how everything fits together. Figure 2-1 shows the μC/OS-III architecture and its relationship with hardware. Of course, in addition to the timer and interrupt controller, hardware would most likely contain such other devices as Universal Asynchronous Receiver Transmitters (UARTs), Analog to Digital Converters (ADCs), Ethernet controller(s) and more. This chapter assumes development on a Windows®-based platform and makes references to typical Windows-type directory structures (also called Folder). However, since μC/OS-III is available in source form, it can also be used on Unix, Linux or other development platforms. The names of the files are shown in upper case to make them “stand out”. However, file names are actually lower case. 33 Chapter 2 μC/OS-III Configuration Application Code OS_CFG.H OS_CFG_APP.H APP.C APP.H (8) (1) μC/OS-III μC/LIB CPU Independent Libraries OS_CFG_APP.C OS_TYPE.H OS_CORE.C OS_DBG.C OS_FLAG.C OS_INT.C OS_MEM.C OS_MSG.C OS_MUTEX.C OS_PEND_MULTI.C OS_PRIO.C OS_Q.C OS_SEM.C OS_STAT.C OS_TASK.C OS_TICK.C OS_TIME.C OS_TMR.C OS.H OS_VAR.C LIB_ASCII.C LIB_ASCII.H LIB_DEF.H LIB_MATH.C LIB_MATH.H LIB_MEM_A.ASM LIB_MEM.C LIB_MEM.H LIB_STR.C LIB_STR.H (4) μC/OS-III μC/CPU BSP CPU Specific CPU Specific Board Support Package (5) (6) OS_CPU.H OS_CPU_A.ASM OS_CPU_C.C CPU (3) CPU.H CPU_A.ASM CPU_CORE.C BSP.C BSP.H (7) (2) *.C *.H Software/Firmware Hardware CPU Timer Interrupt Controller Figure 2-1 μC/OS-III Architecture 34 Directories and Files F2-1(1) The application code consists of project or product files. For convenience, these are simply called APP.C and APP.H, however an application can contain any number of files that do not have to be called APP.*. The application code is typically where one would find main(). F2-1(2) Semiconductor manufacturers often provide library functions in source form for accessing the peripherals on their CPU or MCU. These libraries are quite useful and often save valuable time. Since there is no naming convention for these files, *.C and *.H are assumed. F2-1(3) The Board Support Package (BSP) is code that is typically written to interface to peripherals on a target board. For example such code can turn on and off Light Emitting Diodes (LEDs), turn on and off relays, or code to read switches, temperature sensors, and more. F2-1(4) This is the μC/OS-III processor-independent code. This code is written in highly portable ANSI C and is available to μC/OS-III licensees only. F2-1(5) This is the μC/OS-III code that is adapted to a specific CPU architecture and is called a port. μC/OS-III has its roots in μC/OS-II and benefits from being able to use most of the 45 or so ports available for μC/OS-II. μC/OS-II ports, however, will require small changes to work with μC/OS-III. These changes are described in Appendix C, “Migrating from μC/OS-II to μC/OS-III” on page 599. F2-1(6) At Micriμm, we like to encapsulate CPU functionality. These files define functions to disable and enable interrupts, CPU_??? data types to be independent of the CPU and compiler used, and many more functions. F2-1(7) μC/LIB is of a series of source files that provide common functions such as memory copy, string, and ASCII-related functions. Some are occasionally used to replace stdlib functions provided by the compiler. The files are provided to ensure that they are fully portable from application to application and especially, from compiler to compiler. μC/OS-III does not use these files, but μC/CPU does. F2-1(8) μC/OS-III configuration files defines μC/OS-III features (OS_CFG.H) to include in the application, and specifies the size of certain variables and data structures expected by μC/OS-III (OS_CFG_APP.H), such as idle task stack size, tick rate, size of the message pool, etc. 35 Chapter 2 2-1 APPLICATION CODE When Micriμm provides example projects, they are placed in a directory structure shown below. Of course, a directory structure that suits a particular project/product can be used. \Micrium \Software \EvalBoards \ \ \ \ \*.* \Micrium This is where we place all software components and projects provided by Micriμm. This directory generally starts from the root directory of the computer. \Software This sub-directory contains all software components and projects. \EvalBoards This sub-directory contains all projects related to evaluation boards supported by Micriμm. \ This is the name of the manufacturer of the evaluation board. The “” are not part of the actual name. \ This is the name of the evaluation board. A board from Micriμm will typically be called uC-Eval-xxxx where “xxxx” represents the CPU or MCU used on the board. The “” are not part of the actual name. \ This is the name of the compiler or compiler manufacturer used to build the code for the evaluation board. The “” are not part of the actual name. 36 Directories and Files \ The name of the project that will be demonstrated. For example, a simple μC/OS-III project might have a project name of “OS-Ex1”. The “-Ex1” represents a project containing only μC/OS-III. The project name OS-Probe-Ex1 contains μC/OS-III and μC/Probe. \*.* These are the project source files. Main files can optionally be called APP*.*. This directory also contains configuration files OS_CFG.H, OS_CFG_APP.H and other required source files. 2-2 CPU The directory where you will find semiconductor manufacturer peripheral interface source files is shown below. Any directory structure that suits the project/product may be used. \Micrium \Software \CPU \ \ \*.* \Micrium The location of all software components and projects provided by Micriμm. \Software This sub-directory contains all software components and projects. \CPU This sub-directory is always called CPU. \ Is the name of the semiconductor manufacturer providing the peripheral library. \ The name of the specific library, generally associated with a CPU name or an architecture. \*.* Indicates library source files. The semiconductor manufacturer names the files. 37 Chapter 2 2-3 BOARD SUPPORT PACKAGE (BSP) The Board Support Package (BSP) is generally found with the evaluation or target board as it is specific to that board. In fact, when well written, the BSP should be used for multiple projects. \Micrium \Software \EvalBoards \ \ \ \BSP \*.* \Micrium Contains all software components and projects provided by Micriμm. \Software This sub-directory contains all software components and projects. \EvalBoards This sub-directory contains all projects related to evaluation boards. \ The name of the manufacturer of the evaluation board. The “” are not part of the actual name. \ The name of the evaluation board. A board from Micriμm will typically be called uC-Eval-xxxx where “xxxx” is the name of the CPU or MCU used on the evaluation board. The “” are not part of the actual name. \ The name of the compiler or compiler manufacturer used to build code for the evaluation board. The “” are not part of the actual name. 38 Directories and Files \BSP This directory is always called BSP. \*.* The source files of the BSP. Typically all of the file names start with BSP. It is therefore normal to find BSP.C and BSP.H in this directory. BSP code should contain such functions as LED control functions, initialization of timers, interface to Ethernet controllers and more. 2-4 μC/OS-III, CPU INDEPENDENT SOURCE CODE The files in these directories are available to μC/OS-III licensees (see Appendix F, “Licensing Policy” on page 645). \Micrium \Software \uCOS-III \Cfg\Template \OS_APP_HOOKS.C \OS_CFG.H \OS_CFG_APP.H \Source \OS_CFG_APP.C \OS_CORE.C \OS_DBG.C \OS_FLAG.C \OS_INT.C \OS_MEM.C \OS_MSG.C \OS_MUTEX.C \OS_PEND_MULTI.C \OS_PRIO.C \OS_Q.C \OS_SEM.C \OS_STAT.C \OS_TASK.C \OS_TICK.C \OS_TIME.C 39 Chapter 2 \OS_TMR.C \OS_VAR \OS.H \OS_TYPE.H \Micrium Contains all software components and projects provided by Micriμm. \Software This sub-directory contains all software components and projects. \uCOS-III This is the main μC/OS-III directory. \Cfg\Template This directory contains examples of configuration files to copy to the project directory. You will then modify these files to suit the needs of the application. OS_APP_HOOKS.C shows how to write hook functions that are called by μC/OS-III. Specifically, this file contains eight empty functions. OS_CFG.H specifies which features of μC/OS-III are available for an application. The file is typically copied into an application directory and edited based on which features are required from μC/OS-III. If μC/OS-III is provided in linkable object code format, this file will be provided to indicate features that are available in the object file. See Appendix B, “μC/OS-III Configuration Manual” on page 579. OS_CFG_APP.H is a configuration file to be copied into an application directory and edited based on application requirements. This file enables the user to determine the size of the idle task stack, the tick rate, the number of messages available in the message pool and more. See Appendix B, “μC/OS-III Configuration Manual” on page 579. \Source The directory containing the CPU-independent source code for μC/OS-III. All files in this directory should be included in the build (assuming you have the source code). Features that are not required will be compiled out based on the value of #define constants in OS_CFG.H and OS_CFG_APP.H. 40 Directories and Files OS_CFG_APP.C declares variables and arrays based on the values in OS_CFG_APP.H. OS_CORE.C contains core functionality for μC/OS-III such as OSInit() to initialize μC/OS-III, OSSched() for the task level scheduler, OSIntExit() for the interrupt level scheduler, pend list (or wait list) management (see Chapter 10, “Pend Lists (or Wait Lists)” on page 177), ready list management (see Chapter 6, “The Ready List” on page 123), and more. OS_DBG.C contains declarations of constant variables used by a kernel aware debugger or μC/Probe. OS_FLAG.C contains the code for event flag management. See Chapter 14, “Synchronization” on page 251 for details about event flags. OS_INT.C contains code for the interrupt handler task, which is used when OS_CFG_ISR_POST_DEFERRED_EN (see OS_CFG.H) is set to 1. See Chapter 9, “Interrupt Management” on page 157 for details regarding the interrupt handler task. OS_MEM.C contains code for the μC/OS-III fixed-size memory manager, see Chapter 17, “Memory Management” on page 323. OS_MSG.C contains code to handle messages. μC/OS-III provides message queues and task specific message queues. OS_MSG.C provides common code for these two services. See Chapter 15, “Message Passing” on page 289. OS_MUTEX.C contains code to manage mutual exclusion semaphores, see Chapter 13, “Resource Management” on page 209. OS_PEND_MULTI.C contains the code to allow code to pend on multiple semaphores or message queues. This is described in Chapter 16, “Pending On Multiple Objects” on page 313. OS_PRIO.C contains the code to manage the bitmap table used to keep track of which tasks are ready to run, see Chapter 6, “The Ready List” on page 123. This file can be replaced by an assembly language equivalent to improve performance if the CPU used provides bit set, clear and test instructions, and a count leading zeros instruction. 41 Chapter 2 OS_Q.C contains code to manage message queues. See Chapter 15, “Message Passing” on page 289. OS_SEM.C contains code to manage semaphores used for resource management and/or synchronization. See Chapter 13, “Resource Management” on page 209 and Chapter 14, “Synchronization” on page 251. OS_STAT.C contains code for the statistic task, which is used to compute the global CPU usage and the CPU usage of each of tasks. See Chapter 5, “Task Management” on page 75. OS_TASK.C contains code for managing tasks using OSTaskCreate(), OSTaskDel(), OSTaskChangePrio(), and many more. See Chapter 5, “Task Management” on page 75. OS_TICK.C contains code to manage tasks that have delayed themselves or that are pending on a kernel object with a timeout. See Chapter 5, “Task Management” on page 75. OS_TIME.C contains code to allow a task to delay itself until some time expires. See Chapter 11, “Time Management” on page 183. OS_TMR.C contains code to manage software timers. See Chapter 12, “Timer Management” on page 193. OS_VAR.C contains the μC/OS-III global variables. These variables are for μC/OS-III to manage and should not be accessed by application code. OS.H contains the main μC/OS-III header file, which declares constants, macros, μC/OS-III global variables (for use by μC/OS-III only), function prototypes, and more. OS_TYPE.H contains declarations of μC/OS-III data types that can be changed by the port designed to make better use of the CPU architecture. In this case, the file would typically be copied to the port directory and then modified. μC/OS-III in linkable object library format provides this file to enable the user to know what each data type maps to. See Appendix B, “μC/OS-III Configuration Manual” on page 579. 42 Directories and Files 2-5 μC/OS-III, CPU SPECIFIC SOURCE CODE The μC/OS-III port developer provides these files. See also Chapter 18, “Porting μC/OS-III” on page 335. \Micrium \Software \uCOS-III \Ports \ \ \OS_CPU.H \OS_CPU_A.ASM \OS_CPU_C.C \Micrium Contains all software components and projects provided by Micriμm. \Software This sub-directory contains all software components and projects. \uCOS-III The main μC/OS-III directory. \Ports The location of port files for the CPU architecture(s) to be used. \ This is the name of the CPU architecture that μC/OS-III was ported to. The “” are not part of the actual name. \ The name of the compiler or compiler manufacturer used to build code for the port. The “” are not part of the actual name. The files in this directory contain the μC/OS-III port, see Chapter 18, “Porting μC/OS-III” on page 335 for details on the contents of these files. 43 Chapter 2 OS_CPU.H contains a macro declaration for OS_TASK_SW(), as well as the function prototypes for at least the following functions: OSCtxSw(), OSIntCtxSw() and OSStartHighRdy(). OS_CPU_A.ASM contains the assembly language functions to implement at least the following functions: OSCtxSw(), OSIntCtxSw() and OSStartHighRdy(). OS_CPU_C.C contains the C code for the port specific hook functions and code to initialize the stack frame for a task when the task is created. 2-6 μC/CPU, CPU SPECIFIC SOURCE CODE μC/CPU consists of files that encapsulate common CPU-specific functionality and CPU and compiler-specific data types. See Chapter 18, “Porting μC/OS-III” on page 335. \Micrium \Software \uC-CPU \CPU_CORE.C \CPU_CORE.H \CPU_DEF.H \Cfg\Template \CPU_CFG.H \ \ \CPU.H \CPU_A.ASM \CPU_C.C \Micrium Contains all software components and projects provided by Micriμm. \Software This sub-directory contains all software components and projects. 44 Directories and Files \uC-CPU This is the main μC/CPU directory. CPU_CORE.C contains C code that is common to all CPU architectures. Specifically, this file contains functions to measure the interrupt disable time of the CPU_CRITICAL_ENTER() and CPU_CRITICAL_EXIT() macros, a function that emulates a count leading zeros instruction and a few other functions. CPU_CORE.H contains function prototypes for the functions provided in CPU_CORE.C and allocation of the variables used by the module to measure interrupt disable time. CPU_DEF.H contains miscellaneous #define constants used by the μC/CPU module. \Cfg\Template This directory contains a configuration template file (CPU_CFG.H) that must be copied to the application directory to configure the μC/CPU module based on application requirements. CPU_CFG.H determines whether to enable measurement of the interrupt disable time, whether the CPU implements a count leading zeros instruction in assembly language, or whether it will be emulated in C, and more. \ The name of the CPU architecture that μC/CPU was ported to. The “” are not part of the actual name. \ The name of the compiler or compiler manufacturer used to build code for the μC/CPU port. The “” are not part of the actual name. The files in this directory contain the μC/CPU port, see Chapter 18, “Porting μC/OS-III” on page 335 for details on the contents of these files. CPU.H contains type definitions to make μC/OS-III and other modules independent of the CPU and compiler word sizes. Specifically, one will find the declaration of the CPU_INT16U, CPU_INT32U, CPU_FP32 and many other data types. This file also specifies whether the CPU is a big or little endian machine, defines the CPU_STK data type used by μC/OS-III, defines the macros OS_CRITICAL_ENTER() and OS_CRITICAL_EXIT(), and contains function prototypes for functions specific to the CPU architecture, and more. 45 Chapter 2 CPU_A.ASM contains the assembly language functions to implement code to disable and enable CPU interrupts, count leading zeros (if the CPU supports that instruction), and other CPU specific functions that can only be written in assembly language. This file may also contain code to enable caches, setup MPUs and MMU, and more. The functions provided in this file are accessible from C. CPU_C.C contains C code of functions that are based on a specific CPU architecture but written in C for portability. As a general rule, if a function can be written in C then it should be, unless there is significant performance benefits available by writing it in assembly language. 2-7 μC/LIB, PORTABLE LIBRARY FUNCTIONS μC/LIB consists of library functions meant to be highly portable and not tied to any specific compiler. This facilitates third-party certification of Micriμm products. μC/OS-III does not use any μC/LIB functions, however the μC/CPU assumes the presence of LIB_DEF.H for such definitions as: DEF_YES, DEF_NO, DEF_TRUE, DEF_FALSE, DEF_ON, DEF_OFF and more. \Micrium \Software \uC-LIB \LIB_ASCII.C \LIB_ASCII.H \LIB_DEF.H \LIB_MATH.C \LIB_MATH.H \LIB_MEM.C \LIB_MEM.H \LIB_STR.C \LIB_STR.H \Cfg\Template \LIB_CFG.H \Ports \ \ \LIB_MEM_A.ASM 46 Directories and Files \Micrium Contains all software components and projects provided by Micriμm. \Software This sub-directory contains all software components and projects. \uC-LIB This is the main μC/LIB directory. \Cfg\Template This directory contains a configuration template file (LIB_CFG.H) that are required to be copied to the application directory to configure the μC/LIB module based on application requirements. LIB_CFG.H determines whether to enable assembly language optimization (assuming there is an assembly language file for the processor, i.e., LIB_MEM_A.ASM) and a few other #defines. 2-8 SUMMARY Below is a summary of all directories and files involved in a μC/OS-III-based project. The “@ IUHH XVHG                &XUUHQW 6WDFN 8VDJH /RZ0HPRU\ VL]H IUHHXVHG 6WDFNLQLWLDOL]HGWRDOO]HURV :RUVW&DVHVWDFNJURZWK 6WDFN*URZWK +LJK0HPRU\ &38B67. Figure 5-5 Software detection of stack overflows, walking the stack 90 Task Management 5-4 TASK MANAGEMENT SERVICES μC/OS-III provides a number of task-related services to call from the application. These services are found in OS_TASK.C and they all start with OSTask???(). The type of service they perform groups task-related services: Group Functions General OSTaskCreate() OSTaskDel() OSTaskChangePrio() OSTaskRegSet() OSTaskRegGet() OSTaskSuspend() OSTaskResume() OSTaskTimeQuantaSet() Signaling a Task OSTaskSemPend() (See also Chapter 14, “Synchronization” on page 251) OSTaskSemPost() OSTaskSemPendAbort() Sending Messages to a Task (See also Chapter 15, “Message Passing” on page 289) OSTaskQPend() OSTaskQPost() OSTaskQPendAbort() OSTaskQFlush() Table 5-2 Task Management Services A complete description of all μC/OS-III task related services is provided in Appendix A, “μC/OS-III API Reference Manual” on page 375. 91 Chapter 5 5-5 TASK MANAGEMENT INTERNALS 5-5-1 TASK STATES From a μC/OS-III user point of view, a task can be in any one of five states as shown in Figure 5-6. Internally, μC/OS-III does not need to keep track of the dormant state and the other states are tracked slightly differently. This will be discussed after a discussion on task states from the user’s point of view. Figure 5-6 also shows which μC/OS-III functions are used to move from one state to another. The diagram is actually simplified as state transitions are a bit more complicated than this. 3HQGLQJ  26)ODJ3HQG 260XWH[3HQG 2643HQG 266HP3HQG 267DVN43HQG 267DVN6HP3HQG 267DVN6XVSHQG 267LPH'O\ 267LPH'O\+060 26)ODJ'HO 26)ODJ3HQG$ERUW 26)ODJ3RVW 260XWH['HO 260XWH[3HQG$ERUW 260XWH[3RVW 264'HO 2643HQG$ERUW 2643RVW 266HP'HO 266HP3HQG$ERUW 266HP3RVW 267DVN43HQG$ERUW 267DVN43RVW 267DVN6HP3HQG$ERUW 267DVN6HP3RVW 267DVN5HVXPH 267LPH'O\5HVXPH 267LPH7LFN 267DVN'HO 26,QW([LW 266WDUW 26B7$6.B6: 267DVN&UHDWH 5HDG\  'RUPDQW  267DVN'HO 5XQQLQJ  7DVN3UHHPSWHG 26,QW([LW ,QWHUUXSW 5HWXUQ )URP ,QWHUUXSW ,QWHUUXSWHG  267DVN'HO Figure 5-6 Five basic states of a task 92 Task Management F5-6(1) The Dormant state corresponds to a task that resides in memory but has not been made available to μC/OS-III. A task is made available to μC/OS-III by calling a function to create the task, OSTaskCreate(). The task code actually resides in code space but μC/OS-III needs to be informed about it. When it is no longer necessary for μC/OS-III to manage a task, call the task delete function, OSTaskDel(). OSTaskDel() does not actually delete the code of the task, it is simply not eligible to access the CPU. F5-6(2) The Ready state corresponds to a ready-to-run task, but is not the most important task ready. There can be any number of tasks ready and μC/OS-III keeps track of all ready tasks in a ready list (discussed later). This list is sorted by priority. F5-6(3) The most important ready-to-run task is placed in the Running state. On a single CPU, only one task can be running at any given time. The task selected to run on the CPU is switched in by μC/OS-III from the ready state when the application code calls OSStart(), or when μC/OS-III calls either OSIntExit() or OS_TASK_SW(). As previously discussed, tasks must wait for an event to occur. A task waits for an event by calling one of the functions that brings the task to the pending state if the event has not occurred. F5-6(4) Tasks in the Pending state are placed in a special list called a pend-list (or wait list) associated with the event the task is waiting for. When waiting for the event to occur, the task does not consume CPU time. When the event occurs, the task is placed back into the ready list and μC/OS-III decides whether the newly readied task is the most important ready-to-run task. If this is the case, the currently running task will be preempted (placed back in the ready list) and the newly readied task is given control of the CPU. In other words, the newly readied task will run immediately if it is the most important task. 93 Chapter 5 Note that the OSTaskSuspend() function unconditionally blocks a task and this task will not actually wait for an event to occur but in fact, waits until another task calls OSTaskResume() to make the task ready to run. F5-6(5) Assuming that CPU interrupts are enabled, an interrupting device will suspend execution of a task and execute an Interrupt Service Routine (ISR). ISRs are typically events that tasks wait for. Generally speaking, an ISR should simply notify a task that an event occurred and let the task process the event. ISRs should be as short as possible and most of the work of handling the interrupting devices should be done at the task level where it can be managed by μC/OS-III. ISRs are only allowed to make “Post” calls (i.e., OSFlagPost(), OSQPost(), OSSemPost(), OSTaskQPost() and OSTaskSemPost()). The only post call not allowed to be made from an ISR is OSMutexPost() since mutexes, as will be addressed later, are assumed to be services that are only accessible at the task level. As the state diagram indicates, an interrupt can interrupt another interrupt. This is called interrupt nesting and most processors allow this. However, interrupt nesting easily leads to stack overflow if not managed properly. Internally, μC/OS-III keeps track of task states using the state machine shown in Figure 5-7. The task state is actually maintained in a variable that is part of a data structure associated with each task, the task’s TCB. The task state diagram was referenced throughout the design of μC/OS-III when implementing most of μC/OS-III’s services. The number in parentheses is the state number of the task and thus, a task can be in any one of eight (8) states (see OS.H, OS_TASK_STATE_???). Note that the diagram does not keep track of a dormant task, as a dormant task is not known to μC/OS-III. Also, interrupts and interrupt nesting is tracked differently as will be explained further in the text. This state diagram should be quite useful to understand how to use several functions and their impact on the state of tasks. In fact, I’d highly recommend that the reader bookmark the page of the diagram. 94 Task Management 267DVN6XVSHQG 'HOD\ 6XVSHQGHG  'HOD\  267DVN5HVXPH 267LPH'O\ 267LPH'O\+060 'O\([SLUHVRU 267LPH'O\5HVXPH 'O\ ([SLUHVRU 267DVN'O\5HVXPH 267DVN6XVSHQG 5HDG\  6XVSHQGHG  267DVN5HVXPH 26""3HQG Z7LPHRXW 7LPHRXWRU 7LPHRXWRU 26""3RVW RU 26""3RVW RU 26""3HQG$ERUW RU 26""3HQG$ERUW RU 26""'HO 26""'HO 267DVN6XVSHQG 26""3HQG 26""3RVW RU 263HQG$ERUW RU 26""'HO 3HQG 7LPHRXW 6XVSHQGHG  3HQG 7LPHRXW  26""3RVW RU 26""3HQG$ERUW RU 26""'HO 267DVN5HVXPH 267DVN6XVSHQG 3HQG 6XVSHQGHG  3HQG  267DVN5HVXPH Figure 5-7 μC/OS-III’s internal task state machine F5-7(1) State 0 occurs when a task is ready to run. Every task “wants” to be ready to run as that is the only way it gets to perform their duties. F5-7(2) A task can decide to wait for time to expire by calling either OSTimeDly() or OSTimeDlyHMSM(). When the time expires or the delay is cancelled (by calling OSTimeDlyResume()), the task returns to the ready state. F5-7(3) A task can wait for an event to occur by calling one of the pend (i.e., wait) functions (OSFlagPend(), OSMutexPend(), OSQPend(), OSSemPend(), OSTaskQPend(), or OSTaskSemPend()), and specify to wait forever for the event to occur. The pend terminates when the event occurs (i.e., a task or an ISR performs a “post”), the awaited object is deleted or, another task decides to abort the pend. 95 Chapter 5 F5-7(4) A task can wait for an event to occur as indicated, but specify that it is willing to wait a certain amount of time for the event to occur. If the event is not posted within that time, the task is readied, then the task is notified that a timeout occurred. Again, the pend terminates when the event occurs (i.e., a task or an ISR performs a “post”), the object awaited is deleted or, another task decides to abort the pend. F5-7(5) A task can suspend itself or another task by calling OSTaskSuspend(). The only way the task is allowed to resume execution is by calling OSTaskResume(). Suspending a task means that a task will not be able to run on the CPU until it is resumed by another task. F5-7(6) A delayed task can also be suspended by another task. In this case, the effect is additive. In other words, the delay must complete (or be resumed by OSTimeDlyResume()) and the suspension must be removed (by another task which would call OSTaskResume()) in order for the task to be able to run. F5-7(7) A task waiting on an event to occur may be suspended by another task. Again, the effect is additive. The event must occur and the suspension removed (by another task) in order for the task to be able to run. Of course, if the object that the task is pending on is deleted or, the pend is aborted by another task, then one of the above two condition is removed. The suspension , however, must be explicitly removed. F5-7(8) A task can wait for an event, but only for a certain amount of time, and the task could also be suspended by another task. As one might expect, the suspension must be removed by another task (or the same task that suspended it in the first place), and the event needs to either occur or timeout while waiting for the event. 96 Task Management 5-5-2 TASK CONTROL BLOCKS (TCBs) A task control block (TCB) is a data structure used by kernels to maintain information about a task. Each task requires its own TCB and, for μC/OS-III, the user assigns the TCB in user memory space (RAM). The address of the task’s TCB is provided to μC/OS-III when calling task-related services (i.e., OSTask???() functions). The task control block data structure is declared in OS.H as shown in Listing 5-3. Note that the fields are actually commented in OS.H, and some of the fields are conditionally compiled based on whether or not certain features are desired. Both are not shown here for clarity. Also, it is important to note that even when the user understands what the different fields of the OS_TCB do, the application code must never directly access these (especially change them). In other words, OS_TCB fields must only be accessed by μC/OS-III and not the code. struct os_tcb { CPU_STK void CPU_STK OS_TCB OS_TCB OS_TCB OS_TCB OS_TICK_SPOKE OS_CHAR CPU_STK OS_TASK_PTR void OS_PEND_DATA OS_OBJ_QTY CPU_TS void OS_MSG_SIZE OS_MSG_Q CPU_TS CPU_TS OS_FLAGS OS_OPT OS_FLAGS *StkPtr; *ExtPtr; *StkLimitPtr; *NextPtr; *PrevPtr; *TickNextPtr; *TickPrevPtr; *TickSpokePtr; *NamePtr; *StkBasePtr; TaskEntryAddr; *TaskEntryArg; *PendDataTblPtr; PendDataEntries; TS; *MsgPtr; MsgSize; MsgQ; MsgQPendTime; MsgQPendTimeMax; FlagsPend; FlagsOpt; FlagsRdy; 97 Chapter 5 OS_REG OS_SEM_CTR RegTbl[OS_TASK_REG_TBL_SIZE]; SemCtr; CPU_TS CPU_TS OS_NESTING_CTR SemPendTime; SemPendTimeMax; SuspendCtr; CPU_STK_SIZE CPU_STK_SIZE CPU_STK_SIZE StkSize; StkUsed; StkFree; OS_OPT OS_TICK OS_TICK OS_TICK OS_TICK OS_TICK OS_CPU_USAGE OS_CTX_SW_CTR CPU_TS CPU_TS OS_CYCLES CPU_TS CPU OS_STATE OS_STATUS OS_STATE OS_PRIO OS_TCB OS_TCB CPU_CHAR Opt; TickCtrPrev; TickCtrMatch; TickRemain; TimeQuanta; TimeQuantaCtr; CPUUsage; CtxSwCtr; CyclesDelta; CyclesStart; CyclesTotal; IntDisTimeMax; SchedLockTimeMax; PendOn; PendStatus; TaskState; Prio; DbgNextPtr; DbgPrevPtr; DbgNamePtr; }; Listing 5-3 OS_TCB Data Structure .StkPtr This field contains a pointer to the current top-of-stack for the task. μC/OS-III allows each task to have its own stack and each stack can be any size. .StkPtr should be the only field in the OS_TCB data structure accessed from assembly language code (for the context-switching code). This field is therefore placed as the first entry in the structure making access easier from assembly language code (it will be at offset zero in the data structure). .ExtPtr This field contains a pointer to a user-definable pointer to extend the TCB as needed. This pointer is easily accessible from assembly language. 98 Task Management .StkLimitPtr The field contains a pointer to a location in the task’s stack to set a watermark limit for stack growth and is determined from the value of the “stk_limit” argument passed to OSTaskCreate(). Some processors have special registers that automatically check the value of the stack pointer at run-time to ensure that the stack does not overflow. .StkLimitPtr may be used to set this register during a context switch. Alternatively, if the processor does not have such a register, this can be “simulated” in software. However, it is not as reliable as a hardware solution. If this feature is not used then the value of “stk_limit” can be set to 0 when calling OSTaskCreate(). See also section 5-3 “Detecting Task Stack Overflows” on page 87). 6WDFN 5$0 /RZ0HPRU\ 6WN%DVH3WU 6WN/LPLW3WU 6WN6L]H 6WN3WU &XUUHQW 6WDFN 8VDJH 6WDFN*URZWK +LJK0HPRU\ &38B67. .NextPtr and .PrevPtr These pointers are used to doubly link OS_TCBs in the ready list. A doubly linked list allows OS_TCBs to be quickly inserted and removed from the list. .TickNextPtr and .TickPrevPtr These pointers are used to doubly link OS_TCBs in the list of tasks waiting for time to expire or to timeout from pend calls. Again, a doubly linked list allows OS_TCBs to be quickly inserted and removed from the list. 99 Chapter 5 .TickSpokePtr This pointer is used to know which spoke in the “tick wheel” the task is linked to. The tick wheel will be described in “Chapter 9, “Interrupt Management” on page 157.” .NamePtr This pointer allows a name (an ASCII string) to be assigned to each task. Having a name is useful when debugging, since it is user friendly compared to displaying the address of the OS_TCB. Storage for the ASCII string is assumed to be in user space in code memory (ASCII string declared as a const) or in RAM. .StkBasePtr This field points to the base address of the task’s stack. The stack base is typically the lowest address in memory where the stack for the task resides. A task stack is declared as follows: CPU_STK MyTaskStk[???]; CPU_STK is the data type you must use to declare task stacks and ??? is the size of the stack associated with the task. The base address is always &MyTaskStk[0]. .TaskEntryAddr This field contains the entry address of the task. As previously mentioned, a task is declared as shown below and this field contains the address of MyTask. void MyTask (void *p_arg); .TaskEntryArg This field contains the value of the argument that is passed to the task when the task starts. As previously mentioned, a task is declared as shown below and this field contains the value of p_arg. void MyTask (void *p_arg); .PendDataTblPtr μC/OS-III allows the task to pend on any number of semaphores or message queues simultaneously. This pointer points to a table containing information about the pended objects. 100 Task Management .PendDataEntries This field works with the .PendDataTblPtr, indicating the number of objects a task is pending on at the same time. .TS This field is used to store a “time stamp” of when an event that the task was waiting on occurred. When the task resumes execution, this time stamp is returned to the caller. .MsgPtr When a message is sent to a task, this field contains the message received. This field only exists in a TCB if message queue services (OS_CFG_Q_EN is set to 1 in OS_CFG.H), or task message queue services, are enabled (OS_CFG_TASK_Q_EN is set to 1 in OS_CFG.H) at compile time. .MsgSize When a message is sent to a task, this field contains the size (in number of bytes) of the message received. This field only exists in a TCB if message queue services (OS_CFG_Q_EN is set to 1 in OS_CFG.H), or task message queue services, (OS_CFG_TASK_Q_EN is set to 1 in OS_CFG.H) are enabled at compile time. .MsgQ μC/OS-III allows tasks or ISRs to send messages directly to tasks. Because of this, a message queue is actually built into each TCB. This field only exists in a TCB if task message queue services are enabled at compile time (OS_CFG_TASK_Q_EN is set to 1 in OS_CFG.H). .MsgQ is used by the OSTaskQ???() services. .MsgQPendTime This field contains the amount of time for a message to arrive. When OSTaskQPost() is called, the current time stamp is read and stored in the message. When OSTaskQPend() returns, the current time stamp is read again and the difference between the two times is stored in this variable. A debugger or μC/Probe can be used to indicate the time taken for a message to arrive by displaying this field. This field is only available if setting OS_CFG_TASK_PROFILE_EN to 1 in OS_CFG.H. 101 Chapter 5 .MsgQPendTimeMax This field contains the maximum amount of time it takes for a message to arrive. It is a peak detector of the value of .MsgQPendTime. The peak can be reset by calling OSStatReset(). This field is only available if setting OS_CFG_TASK_PROFILE_EN to 1 in OS_CFG.H. .FlagsPend When a task pends on event flags, this field contains the event flags (i.e., bits) that the task is pending on. This field only exists in a TCB if event flags services are enabled at compile time (OS_CFG_FLAG_EN is set to 1 in OS_CFG.H). .FlagsOpt When a task pends on event flags, this field contains the type of pend (pend on any event flag bit specified in .FlagsPend or all event flag bits specified in .FlagsPend). This field only exists in a TCB if event flags services are enabled at compile time (OS_CFG_FLAG_EN is set to 1 in OS_CFG.H). .FlagsRdy This field contains the event flags that were posted and that the task was waiting on. In other words, it allows a task to know which event flags made the task ready to run. This field only exists in a TCB if event flags services are enabled at compile time (OS_CFG_FLAG_EN is set to 1 in OS_CFG.H). .RegTbl[] This field contains a table of “registers” that are task-specific. These registers are different than CPU registers. Task registers allow for the storage of such task-specific information as task ID, “errno” common in some software components, and more. Task registers may also store task-related data that needs to be associated with the task at run time. Note that the data type for elements of this array is OS_REG, which can be declared at compile time to be nearly anything. However, all registers must be of this data type. This field only exists in a TCB if task registers are enabled at compile time (OS_CFG_TASK_REG_TBL_SIZE is greater than 0 in OS_CFG.H). .SemCtr This field contains a semaphore counter associated with the task. Each task has its own semaphore built-in. An ISR or another task can signal a task using this semaphore. .SemCtr is therefore used to keep track of how many times the task is signaled. .SemCtr is used by OSTaskSem???() services. 102 Task Management .SemPendTime This field contains the amount of time taken for the semaphore to be signaled. When OSTaskSemPost() is called, the current time stamp is read and stored in the OS_TCB (see .TS). When OSTaskSemPend() returns, the current time stamp is read again and the difference between the two times is stored in this variable. This field can be displayed by a debugger or μC/Probe to indicate how much time it took for the task to be signaled. This field is only available when setting OS_CFG_TASK_PROFILE_EN to 1 in OS_CFG.H. .SemPendTimeMax This field contains the maximum amount of time it took for the task to be signaled. It is a peak detector of the value of .SemPendTime. The peak can be reset by calling OSStatReset(). This field is only available if setting OS_CFG_TASK_PROFILE_EN to 1 in OS_CFG.H. .SuspendCtr This field is used by OSTaskSuspend() and OSTaskResume() to keep track of how many times a task is suspended. Task suspension can be nested. When .SuspendCtr is 0, all suspensions are removed. This field only exists in a TCB if task suspension is enabled at compile time (OS_CFG_TASK_SUSPEND_EN is set to 1 in OS_CFG.H). .StkSize This field contains the size (in number of CPU_STK elements) of the stack associated with the task. Recall that a task stack is declared as follows: CPU_STK MyTaskStk[???]; .StkSize is the value of ??? in the above array. .StkUsed and .StkFree μC/OS-III is able to compute (at run time) the amount of stack space a task actually uses and how much stack space remains. This is accomplished by a function called OSTaskStkChk(). Stack usage computation assumes that the task’s stack is “cleared” when the task is created. In other words, when calling OSTaskCreate(), it is expected that the following options be specified: OS_TASK_OPT_STK_CLR and OS_TASK_OPT_STK_CHK. OSTaskCreate() will then clear all the RAM used for the task’s stack. 103 Chapter 5 μC/OS-III provides an internal task called OS_StatTask() that checks the stack of each of the tasks at run-time. OS_StatTask() typically runs at a low priority so that it does not interfere with the application code. OS_StatTask() saves the value computed for each task in the TCB of each task in these fields, which represents the maximum number of stack bytes used and the amount of stack space still unused by the task. These fields only exist in a TCB if the statistic task is enabled at compile time (OS_CFG_STAT_TASK_STK_CHK_EN is set to 1 in OS_CFG.H). .Opt This field saves the “options” passed to OSTaskCreate() when the task is created (see OS_TASK_OPT_??? in OS.H). Note that task options are additive. .TickCtrPrev This field stores the previous value of OSTickCtr when OSTimeDly() is called with the OS_OPT_TIME_PERIODIC option. .TickCtrMatch When a task is waiting for time to expire, or pending on an object with a timeout, the task is placed in a special list of tasks waiting for time to expire. When in this list, the task waits for .TickCtrMatch to match the value of the “tick counter” (OSTickCtr). When a match occurs, the task is removed from that list. .TickRemain This field is computed at run time by OS_TickTask() to compute the amount of time (expressed in “ticks”) left before a delay or timeout expires. This field is useful for debuggers or run-time monitors for display purposes. .TimeQuanta and .TimeQuantaCtr These fields are used for time slicing. When multiple tasks are ready to run at the same priority, .TimeQuanta determines how much time (in ticks) the task will execute until it is preempted by μC/OS-III so that the next task at the same priority gets a chance to execute. .TimeQuantaCtr keeps track of the remaining number of ticks for this to happen and is loaded with .TimeQuanta at the beginning of the task’s time slice. .CPUUsage This field is computed by OS_StatTask() if OS_CFG_TASK_PROFILE_EN is set to 1 in OS_CFG.H. .CPUUsage contains the CPU usage of a task in percent (0 to 100%). 104 Task Management .CtxSwCtr This field keeps track of how often the task has executed (not how long it has executed). This field is generally used by debuggers or run-time monitors to see if a task is executing (the value of this field would be non-zero and would be incrementing). The field is enabled at compile time when OS_CFG_TASK_PROFILE_EN is set to 1. .CyclesDelta .CyclesDelta is computed during a context switch and contains the value of the current time stamp (obtained by calling OS_TS_GET()) minus the value of .CyclesStart. This field is generally used by debuggers or a run-time monitor to see how long a task takes to execute. The field is enabled at compile time when OS_CFG_TASK_PROFILE_EN is set to 1. .CyclesStart This field is used to measure the execution time of a task. .CyclesStart is updated when μC/OS-III performs a context switch. .CyclesStart contains the value of the current time stamp (it calls OS_TS_GET()) when a task switch occurs. This field is generally used by debuggers or a run-time monitor to see how long a task takes to execute. The field is enabled at compile time when OS_CFG_TASK_PROFILE_EN is set to 1. .CyclesTotal This field accumulates the value of .CyclesDelta, so it contains the total execution time of a task. This is typically a 64-bit value because of the accumulation of cycles over time. Using a 64-bit value ensures that we can accumulate CPU cycles for almost 600 years even if the CPU is running at 1 GHz! Of course, it’s assumed that the compiler supports 64-bit data types. .IntDisTimeMax This field keeps track of the maximum interrupt disable time of the task. The field is updated only if μC/CPU supports interrupt disable time measurements. This field is available only if setting OS_CFG_TASK_PROFILE_EN to 1 in OS_CFG.H and μC/CPU’s CPU_CFG_TIME_MEAS_INT_DIS_EN is defined in DCPU_CFG.H. .SchedLockTimeMax The field keeps track of the maximum scheduler lock time of the task. This field is available only if you set OS_CFG_TASK_PROFILE_EN OS_CFG_SCHED_LOCK_TIME_MEAS_EN is set to 1 in OS_CFG.H. to 1 and 105 Chapter 5 .PendOn This field indicates upon what the task is pending and contains OS_TASK_PEND_ON_??? (see OS.H). .PendStatus This field indicates the outcome of a pend and contains OS_STATUS_PEND_??? (see OS.H). .TaskState This field indicates the current state of a task and contains one of the eight (8) task states that a task can be in, see OS_TASK_STATE_??? (see OS.H). .Prio This field contains the current priority of a task. .Prio is a value between 0 and OS_CFG_PRIO_MAX-1. In fact, the idle task is the only task at priority OS_CFG_PRIO_MAX-1. .DbgNextPtr This field contains a pointer to the next OS_TCB in a doubly linked list of OS_TCBs. OS_TCBs are placed in this list by OSTaskCreate(). This field is only present if OS_CFG_DBG_EN is set to 1 in OS_CFG.H. the current priority of a task. .DbgPrevPtr This field contains a pointer to the previous OS_TCB in a doubly linked list of OS_TCBs. OS_TCBs are placed in this list by OSTaskCreate(). This field is only present if OS_CFG_DBG_EN is set to 1 in OS_CFG.H. .DbgNamePtr This field contains a pointer to the name of the object that the task is pending on when the task is pending on an event flag group, a semaphore, a mutual exclusion semaphore or a message queue. This information is quite useful during debugging and thus, this field is only present if OS_CFG_DBG_EN is set to 1 in OS_CFG.H. 5-6 INTERNAL TASKS During initialization, μC/OS-III creates a minimum of two (2) internal tasks (OS_IdleTask() and OS_TickTask()) and, three (3) optional tasks (OS_StatTask(), OS_TmrTask() and OS_IntQTask()). The optional tasks are created based on the value of compile-time #defines found in OS_CFG.H. 106 Task Management 5-6-1 THE IDLE TASK (OS_IdleTask()) OS_IdleTask() is the very first task created by μC/OS-III and always exists in a μC/OS-IIIbased application. The priority of the idle task is always set to OS_CFG_PRIO_MAX-1. In fact, OS_IdleTask() is the only task that is ever allowed to be at this priority and, as a safeguard, when other tasks are created, OSTaskCreate() ensures that there are no other tasks created at the same priority as the idle task. The idle task runs whenever there are no other tasks that are ready to run. The important portions of the code for the idle task are shown below (refer to OS_CORE.C for the complete code). void OS_IdleTask (void *p_arg) { while (DEF_ON) { OS_CRITICAL_ENTER(); OSIdleTaskCtr++; OSTaskStatCtr++; OS_CRITICAL_EXIT(); OSIdleTaskHook(); } } (1) (2) (3) Listing 5-4 Idle Task L5-4(1) The idle task is a “true” infinite loop that never calls functions to “wait for an event”. This is because, on most processors, when there is “nothing to do,” the processor still executes instructions. When μC/OS-III determines that there is no other higher-priority task to run, μC/OS-III “parks” the CPU in the idle task. Instead of having an empty “for loop” doing nothing, this “idle” time is used to do something useful. L5-4(2) Two counters are incremented whenever the idle task runs. OSIdleTaskCtr is typically defined as a 32-bit unsigned integer (see OS.H). OSIdleTaskCtr is reset once when μC/OS-III is initialized. OSIdleTaskCtr is used to indicate “activity” in the idle task. In other words, if one monitors and displays OSIdleTaskCtr, one should expect to see a value between 0x00000000 and 0xFFFFFFFF. The rate at which OSIdleTaskCtr increments depend on how busy the CPU is at running the application code. The faster the increment, the less work the CPU has to do in application tasks. 107 Chapter 5 OSStatTaskCtr is also typically defined as a 32-bit unsigned integer (see OS.H) and is used by the statistic task (described later) to get a sense of CPU utilization at run time. L5-4(3) Every time through the loop, OS_IdleTask() calls OSIdleTaskHook(), which is a function that is declared in the μC/OS-III port for the processor used. OSIdleTaskHook() allows the implementer of the μC/OS-III port to perform additional processing during idle time. It is very important for this code to not make calls that would cause the idle task to “wait for an event”. This is generally not a problem as most programmers developing μC/OS-III ports know to follow this simple rule. OSIdleTaskHook() may be used to place the CPU in low-power mode for battery-powered applications or to simply not waste energy as shown in the pseudo-code below. However, doing this means that OSStatTaskCtr cannot be used to measure CPU utilization (described later). void OSIdleTaskHook (void) { /* Place the CPU in low power mode */ } Typically, most processors exit low-power mode when an interrupt occurs. Depending on the processor, however, the Interrupt Service Routine (ISR) may have to write to “special” registers to return the CPU to its full or desired speed. If the ISR wakes up a high-priority task (every task is higher in priority than the idle task) then the ISR will not immediately return to the interrupted idle task, but instead switch to the higher-priority task. When the higher-priority task completes its work and waits for its event to occur, μC/OS-III causes a context switch to return to OSIdleTaskHook() just “after” the instruction that caused the CPU to enter low-power mode. In turn, OSIdleTaskHook() returns to OS_IdleTask() and causes another iteration through the “for loop.” 108 Task Management 5-6-2 THE TICK TASK (OS_TickTask()) Nearly every RTOS requires a periodic time source called a Clock Tick or System Tick to keep track of time delays and timeouts. μC/OS-III’s clock tick handling is encapsulated in the file OS_TICK.C. OS_TickTask() is a task created by μC/OS-III and its priority is configurable by the user through μC/OS-III’s configuration file OS_CFG_APP.H (see OS_CFG_TICK_TASK_PRIO). Typically OS_TickTask() is set to a relatively high priority. In fact, the priority of this task is set slightly lower than the most important tasks. OS_TickTask() is used by μC/OS-III to keep track of tasks waiting for time to expire or, for tasks that are pending on kernel objects with a timeout. OS_TickTask() is a periodic task and it waits for signals from the tick ISR (described in Chapter 9, “Interrupt Management” on page 157) as shown in Figure 5-8. Timer (1) 10 to 1000 Hz (3) Tick Task Tick ISR (2) (4) N List of tasks waiting for time to expire or to timeout Figure 5-8 Tick ISR and Tick Task relationship F5-8(1) A hardware timer is generally used and configured to generate an interrupt at a rate between 10 and 1000 Hz (see OS_CFG_TICK_RATE in OS_CFG_APP.H). This timer is generally called the Tick Timer. The actual rate to use depends on such factors as: processor speed, desired time resolution, and amount of allowable overhead to handle the tick timer, etc. The tick interrupt does not have to be generated by a timer and, in fact, it can come from other regular time sources such as the power-line frequency (50 or 60 Hz), which are known to be fairly accurate over long periods of time. 109 Chapter 5 F5-8(2) Assuming CPU interrupts are enabled, the CPU accepts the tick interrupt, preempts the current task, and vectors to the tick ISR. The tick ISR must call OSTimeTick() (see OS_TIME.C), which accomplishes most of the work needed by μC/OS-III. The tick ISR then clears the timer interrupt (and possibly reloads the timer for the next interrupt). However, some timers may need to be taken care of prior to calling OSTimeTick() instead of after as shown below. void TickISR (void) { OSTimeTick(); /* Clear tick interrupt source */ /* Reload the timer for the next interrupt */ } or, void TickISR (void) { /* Clear tick interrupt source */ /* Reload the timer for the next interrupt */ OSTimeTick(); } OSTimeTick() calls OSTimeTickHook() at the very beginning of OSTimeTick() to give the opportunity to the μC/OS-III port developer to react as soon as possible upon servicing the tick interrupt. F5-8(3) OSTimeTick() calls a service provided by μC/OS-III to signal the tick task and make that task ready to run. The tick task executes as soon as it becomes the most important task. The reason the tick task might not run immediately is that the tick interrupt could have interrupted a task higher in priority than the tick task and, upon completion of the tick ISR, μC/OS-III will resume the interrupted task. F5-8(4) When the tick task executes, it goes through a list of all tasks that are waiting for time to expire or are waiting on a kernel object with a timeout. From this point forward, this will be called the tick list. The tick task will make ready to run all of the tasks in the tick list for which time or timeout has expired. The process is explained below. 110 Task Management μC/OS-III may need to place literally hundreds of tasks (if an application has that many tasks) in the tick list. The tick list is implemented in such a way that it does not take much CPU time to determine if time has expired for those tasks placed in the tick list and, possibly makes those tasks ready to run. The tick list is implemented as shown in Figure 5-9. 26&IJB7LFN:KHHO>@ >@ >@ >@ >@ >@                  267LFN&WU >26B&)*B7,&.B:+((/B6,=(@  >26B&)*B7,&.B:+((/B6,=(@     1EU(QWULHV0D[    )LUVW3WU 1EU(QWULHV Figure 5-9 Empty Tick List F5-9(1) The tick list consists of a table (OSCfg_TickWheel[]) and a counter (OSTickCtr). F5-9(2) The table contains up to OS_CFG_TICK_WHEEL_SIZE entries, which is a compile time configuration value (see OS_CFG_APP.H). The number of entries depends on the amount of memory (RAM) available to the processor and the maximum number of tasks in the application. A good starting point for OS_CFG_TICK_WHEEL_SIZE may be: #Tasks / 4. It is recommended not to make OS_CFG_TICK_WHEEL_SIZE an even multiple of the tick rate. If the tick rate is 1000 Hz and one has 50 tasks in the application, avoid setting OS_CFG_TICK_WHEEL_SIZE to 10 or 20 (use 11 or 23 instead). Actually, prime numbers are good choices. Although it is not really possible to plan at compile time what will happen at run time, ideally, the number of tasks waiting in each entry of the table will be distributed uniformly. 111 Chapter 5 F5-9(3) Each entry in the table contains three fields: .NbrEntriesMax, .NbrEntries and .FirstPtr. .NbrEntries indicates the number of tasks linked to this table entry. .NbrEntriesMax keeps track of the highest number of entries in the table. This value is reset when the application code calls OSStatReset(). .FirstPtr contains a pointer to a doubly linked list of tasks (through the tasks OS_TCB) belonging to the list, at that table position. The counter is incremented by OS_TickTask() each time the task is signaled by the tick ISR. Tasks are automatically inserted in the tick list when the application programmer calls a OSTimeDly???() function, or when an OS???Pend() call is made with a non-zero timeout value. Example 5-1 Using an example to illustrate the process of inserting a task in the tick list, let’s assume that the tick list is completely empty, OS_CFG_TICK_WHEEL_SIZE is configured to 12, and the current value of OSTickCtr is 10 as shown in Figure 5-10. A task is placed in the tick list when OSTimeDly() is called and assume OSTimeDly() is called as follows: : OSTimeDly(1, OS_OPT_TIME_DLY, &err); : Referring to the μC/OS-III reference manual in Appendix A, notice that this action indicates to μC/OS-III to delay the current task for 1 tick. Since OSTickCtr has a value of 10, the task will be put to sleep until OSTickCtr reaches 11 or at the very next clock tick interrupt. Tasks are inserted in the OSCfg_TickWheel[] table using the following equation: MatchValue = OSTickCtr + dly Index into OSCfg_TickWheel[] = MatchValue % OS_CFG_TICK_WHEEL_SIZE Where “dly” is the value passed in the first argument of OSTimeDly() or, 1 in this example. We therefore obtain the following: 112 Task Management MatchValue = 10 + 1 Index into OSCfg_TickWheel[] = (10 + 1) % 12 or, MatchValue = 11 Index into OSCfg_TickWheel[] = 11 Because of the “circular” nature of the table (a modulo operation using the size of the table), the table is referred to as a tick wheel and each entry is a spoke in the wheel. The OS_TCB of the task being delayed is entered at index 11 in OSCfg_TickWheel[] (i.e., spoke 11 using the wheel analogy). The OS_TCB of the task is inserted in the first entry of the list (i.e., pointed to by OSCfg_TickWheel[11].FirstPtr), and the number of entries at spoke 11 is incremented (i.e., OSCfg_TickWheel[11].NbrEntries will be 1). Notice that the OS_TCB also links back to &OSCfg_TickWheel[11] and the “MatchValue” are placed in the OS_TCB field .TickCtrMatch. Since this is the first task inserted in the tick list at spoke 11, the .TickNextPtr and .TickPrevPtr both point to NULL. 26&IJB7LFN:KHHO>@ >@ >@ >@ >@ >@ >@ >@ >@ >@ >@ >@ >@                               7LFN6SRNH3WU  1EU(QWULHV0D[      267LFN&WU 7LFN1H[W3WU )LUVW3WU 1EU(QWULHV   7LFN3UHY3WU 7LFN5HPDLQ  7LFN&WU0DWFK  26B7&% Figure 5-10 Inserting a task in the tick list 113 Chapter 5 OSTimeDly() takes care of a few other details. Specifically, the task is removed from μC/OS-III’s ready list (described in Chapter 6, “The Ready List” on page 123) since the task is no longer eligible to run (because it is waiting for time to expire). Also, the scheduler is called because μC/OS-III will need to run the next most important ready-to-run task. If the next task to run also happens to call OSTimeDly() “before” the next tick arrives and calls OSTimeDly() as follows: : OSTimeDly(13, OS_OPT_TIME_DLY, &err); : μC/OS-III will calculate the match value and spoke as follows: MatchValue = 10 + 13 OSCfg_TickWheel[] spoke number = (10 + 13) % 12 or, MatchValue = 23 OSCfg_TickWheel[] spoke number = 11 The “second task” will be inserted at the same table entry as shown in Figure 5-11. Tasks sharing the same spoke are sorted in ascending order such that the task with the least amount of time remaining is placed at the head of the list. 114 Task Management 26&IJB7LFN:KHHO>@ >@ >@ >@ >@ >@                >@ 267LFN&WU >@  >@ >@ )LUVW7DVN 6HFRQG7DVN >@ >@  >@    1EU(QWULHV0D[  7LFN6SRNH3WU 7LFN1H[W3WU )LUVW3WU 1EU(QWULHV  7LFN3UHY3WU 7LFN5HPDLQ 7LFN6SRNH3WU  7LFN1H[W3WU  7LFN3UHY3WU 7LFN5HPDLQ  7LFN&WU0DWFK  7LFN&WU0DWFK  26B7&% 26B7&% Figure 5-11 Inserting a second task in the tick list When the tick task executes (see OS_TickTask() and also OS_TickListUpdate() in OS_TICK.C), it starts incrementing OSTickCtr and determines which table entry (i.e., which spoke) needs to be processed. Then, if there are tasks in the list at this entry (i.e., .FirstPtr is not NULL), each OS_TCB is examined to determine whether the .TickCtrMatch value “matches” OSTickCtr and, if so, we remove the OS_TCB from the list. If the task is only waiting for time to expire, it will be placed in the ready list (described later). If the task is pending on an object, not only will the task be removed from the tick list, but it will also be removed from the list of tasks waiting on that object. The search through the list terminates as soon as OSTickCtr does not match the task’s .TickCtrMatch value; since there is no point in looking any further in the list. Note that OS_TickTask() does most of its work in a critical section when the tick list is updated. However, because the list is sorted, the critical section has a chance to be fairly short. 115 Chapter 5 5-6-3 THE STATISTIC TASK (OS_StatTask()) μC/OS-III contains an internal task that provides such run-time statistics as overall CPU utilization (0 to 100%), per-task CPU utilization (0-100%), and per-task stack usage. The statistic task is optional in a μC/OS-III application and its presence is controlled by a compile-time configuration constant OS_CFG_STAT_TASK_EN defined in OS_CFG.H. Specifically, the code is included in the build when OS_CFG_STAT_TASK_EN is set to 1. Also, the priority of this task and the location and size of the statistic task’s stack is configurable via OS_CFG_APP.H (OS_CFG_STAT_TASK_PRIO). If the application uses the statistic task, it should call OSStatTaskCPUUsageInit() from the first, and only the application task created in the main() function as shown in Listing 5-5. The startup code should create only one task before calling OSStart(). The single task created is, of course, allowed to create other tasks, but only after calling OSStatTaskCPUUsageInit(). 116 Task Management void main (void) (1) { OS_ERR err; : OSInit(&err); (2) if (err != OS_ERR_NONE) { /* Something wasn’t configured properly, μC/OS-III not properly initialized } /* (3) Create ONE task (we’ll call it AppTaskStart() for sake of discussion) : OSStart(&err); (4) */ */ } void AppTaskStart (void *p_arg) { OS_ERR err; : /* (5) Initialize the tick interrupt #if OS_CFG_STAT_TASK_EN > 0 OSStatTaskCPUUsageInit(&err); (6) #endif : /* (7) Create other tasks while (DEF_ON) { /* AppTaskStart() body } } */ */ */ Listing 5-5 Proper startup for computing CPU utilization L5-5(1) The C compiler should start up the CPU and bring it to main() as is typical in most C applications. L5-5(2) main() calls OSInit() to initialize μC/OS-III. It is assumed that the statistics task is enabled by setting OS_CFG_STAT_TASK_EN to 1 in OS_CFG_APP.H. Always examine μC/OS-III’s returned error code to make sure the call was done properly. Refer to OS.H for a list of possible errors, OS_ERR_???. L5-5(3) As the comment indicates, creates a single task called AppTaskStart() in the example (its name is left to the creator’s discretion). When creating this task, give it a fairly high priority (do not use priority 0 since it’s reserved for μC/OS-III). 117 Chapter 5 Normally, μC/OS-III allows the user to create as many tasks as are necessary prior to calling OSStart(). However, when the statistic task is used to compute overall CPU utilization, it is necessary to create only one task. L5-5(4) Call OSStart() to let μC/OS-III start the highest-priority task which, should be AppTaskStart(). At this point, there should be either four or five tasks created (the timer task is optional): μC/OS-III creates up to four tasks (OS_IdleTask(), OS_TickTask(), OS_StatTask() and OS_TaskTmr()), and now AppTaskStart(). L5-5(5) The start task should then configure and enable tick interrupts. This most likely requires that the user initialize the hardware timer used for the clock tick and have it interrupt at the rate specified by OS_CFG_STAT_TASK_RATE (see OS_CFG_APP.H). Additionally, Micriμm provides sample projects that include a basic board-support package (BSP). The BSP initializes many aspects of the CPU as well as the periodic time source required by μC/OS-III. If available, the user may utilize BSP services by calling BSP_Init() from the startup task. After this point, no further time source initialization is required by the user. L5-5(6) Call OSStatTaskCPUUsageInit(). This function determines the maximum value that OSStatTaskCtr (see OS_IdleTask()) can count up to for 1/OS_CFG_STAT_TASK_RATE second when there are no other tasks running in the system (apart for the other μC/OS-III tasks). For example, if the system does not contain an application task and OSStatTaskCtr counts from 0 to 10,000,000 for 1/OS_CFG_STAT_TASK_RATE second, when adding tasks, and the test is redone every 1/OS_CFG_STAT_TASK_RATE second, the OSStatTaskCtr will not reach 10,000,000 and actual CPU utilization is determined as follows: For example, if when redoing the test, OSStatTaskCtr reaches 7,500,000 the CPU is busy 25% of its time running application tasks: 118 Task Management L5-5(7) AppTaskStart() can then create other application tasks as needed. As previously described, μC/OS-III stores run-time statistics for a task in each task’s OS_TCB. OS_StatTask() also computes stack usage of all created tasks by calling OSTaskStkChk() and stores the return values of this function (free and used stack space) in the .StkFree and .StkUsed field of the task’s OS_TCB, respectively. 5-6-4 THE TIMER TASK (OS_TmrTask()) μC/OS-III provides timer services to the application programmer. Code to handle timers is found in OS_TMR.C. The timer task is optional in a μC/OS-III application and its presence is controlled by the compile-time configuration constant OS_CFG_TMR_EN defined in OS_CFG.H. Specifically, the code is included in the build when OS_CFG_TMR_EN is set to 1. Timers are countdown counters that perform an action when the counter reaches zero. The action is provided by the user through a callback function. A callback function is a function that the user declares and that will be called when the timer expires. The callback can thus be used to turn on or off a light, a motor, or perform whatever action needed. It is important to note that the callback function is called from the context of the timer task. The application programmer may create an unlimited number of timers (limited only by the amount of available RAM). Timer management is fully described in Chapter 12, “Timer Management” on page 193 and the timer services available to the application programmer are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. OS_TmrTask() is a task created by μC/OS-III (this assumes setting OS_CFG_TMR_EN to 1 in OS_CFG.H) and its priority is configurable by the user through μC/OS-III’s configuration file OS_CFG_APP.H (see OS_CFG_TMR_TASK_PRIO). OS_TmrTask() is typically set to a medium priority. 119 Chapter 5 OS_TmrTask() is a periodic task using the same interrupt source that was used to generate clock ticks. However, timers are generally updated at a slower rate (i.e., typically 10 Hz) and the timer tick rate is divided down in software. In other words, if the tick rate is 1000 Hz and the desired timer rate is 10 Hz, the timer task will be signaled every 100th tick interrupt as shown in Figure 5-12. 7LPHU WR+] 7LFN ,65 6LJQDOHG DW 7LFN5DWH7LPHU5DWH 7LPHU 7DVN 1 /LVWRIWLPHUVWRXSGDWH Figure 5-12 Tick ISR and Timer Task relationship 5-6-5 THE ISR HANDLER TASK (OS_IntQTask()) When setting the compile-time configuration constant OS_CFG_ISR_POST_DEFERRED_EN in OS_CFG.H to 1, μC/OS-III creates a task (called OS_IntQTask()) responsible for “deferring” the action of OS service post calls from ISRs. As described in Chapter 4, “Critical Sections” on page 69, μC/OS-III manages critical sections either by disabling/enabling interrupts, or by locking/unlocking the scheduler. If selecting the latter method (i.e., setting OS_CFG_ISR_POST_DEFERRED_EN to 1), μC/OS-III “post” functions called from interrupts are not allowed to manipulate such internal data structures as the ready list, pend lists, and others. When an ISR calls one of the “post” functions provided by μC/OS-III, a copy of the data posted and the desired destination is placed in a special “holding” queue. When all nested ISRs complete, μC/OS-III context switches to the ISR handler task (OS_IntQTask()), which “re-posts” the information placed in the holding queue to the appropriate task(s). This extra step is performed to reduce the amount of interrupt disable time that would otherwise be necessary to remove tasks from wait lists, insert them in the ready list, and perform other time-consuming operations. 120 Task Management /RFN8QORFN6FKHGXOHU 'LVDEOH(QDEOH,QWHUUXSWV ,65 +DQGOHU 7DVN ,65 26)ODJ3RVW 2643RVW 266HP3RVW 267DVN43RVW 267DVN6HP3RVW +ROGLQJ4XHXH 127DOORZHGWRDFFHVV—&26,,, V 5HDG\/LVW 3HQG /LVWVHWF 7DVN 3ULRULW\ 26)ODJ3RVW 2643RVW 266HP3RVW 267DVN43RVW 267DVN6HP3RVW $OORZHGWRDFFHVV—&26,,, V 5HDG\/LVW 3HQG /LVWVHWF Figure 5-13 ISR Handler Task OS_IntQTask() is created by μC/OS-III and always runs at priority 0 (i.e., the highest priority). If OS_CFG_ISR_POST_DEFERRED_EN is set to 1, no other task will be allowed to use priority 0. 5-7 SUMMARY A task is a simple program that thinks it has the CPU all to itself. On a single CPU, only one task executes at any given time. μC/OS-III supports multitasking and allows the application to have any number of tasks. The maximum number of tasks is actually only limited by the amount of memory (both code and data space) available to the processor. A task can be implemented as a run-to-completion task in which the task deletes itself when it is finished or more typically as an infinite loop, waiting for events to occur and processing those events. A task needs to be created. When creating a task, it is necessary to specify the address of an OS_TCB to be used by the task, the priority of the task, and an area in RAM for the task’s stack. A task can also perform computations (CPU bound task), or manage one or more I/O (Input/Output) devices. μC/OS-III creates up to five internal tasks: the idle task, tick task, ISR handler task, statistics task, and timer task. The idle and tick tasks are always created while statistics and timer tasks are optional. 121 Chapter 5 122 Chapter 6 The Ready List Tasks that are ready to execute are placed in the Ready List. The ready list consists of two parts: a bitmap containing the priority levels that are ready and a table containing pointers to all the tasks ready. 123 Chapter 6 6-1 PRIORITY LEVELS Figures 5-1 to 5-3 show the bitmap of priorities that are ready. The “width” of the table depends on the data type CPU_DATA (see CPU.H), which can either be 8-, 16- or 32-bits. The width depends on the processor used. μC/OS-III allows up to OS_CFG_PRIO_MAX different priority levels (see OS_CFG.H). In μC/OS-III, a low-priority number corresponds to a high-priority level. Priority level zero (0) is thus the highest priority level. Priority OS_CFG_PRIO_MAX-1 is the lowest priority level. μC/OS-III uniquely assigns the lowest priority to the idle task. No other tasks are allowed at this priority level. If there are tasks that are ready-to-run at a given a priority level, then its corresponding bit is set (i.e., 1) in the bitmap table. Notice in Figures 5-1 to 5-3 that “priority levels” are numbered from left to right and, the priority level increases (moves toward lower priority) with an increase in table index. The order was chosen to be able to use a special instruction called Count Leading Zeros (CLZ), which is found on many modern processors. This instruction greatly accelerates the process of determining the highest priority level. +LJKHVW 3ULRULW\ 7DVN ELWV  >@ 3ULRULWLHVWR >@ 3ULRULWLHVWR >@ 3ULRULWLHVWR 3ULRULWLHV 26B&)*B35,2B0$; WR 26B&)*B35,2B0$; >26B35,2B7%/B6,=(@   /RZHVW 3ULRULW\ 7DVN Figure 6-1 CPU_DATA declared as a CPU_INT08U 124 The Ready List +LJKHVW 3ULRULW\ 7DVN ELWV  >@ 3ULRULWLHVWR >@ 3ULRULWLHVWR >@ 3ULRULWLHVWR 3ULRULWLHV 26B&)*B35,2B0$; WR 26B&)*B35,2B0$; >26B35,2B7%/B6,=(@   /RZHVW 3ULRULW\ 7DVN Figure 6-2 CPU_DATA declared as a CPU_INT16U +LJKHVW 3ULRULW\ 7DVN ELWV  >@ 3ULRULWLHVWR >@ 3ULRULWLHVWR >@ 3ULRULWLHVWR 3ULRULWLHV 26B&)*B35,2B0$; WR 26B&)*B35,2B0$; >26B35,2B7%/B6,=(@   /RZHVW 3ULRULW\ 7DVN Figure 6-3 CPU_DATA declared as a CPU_INT32U 125 Chapter 6 OS_PRIO.C contains the code to set, clear, and search the bitmap table. These functions are internal to μC/OS-III and are placed in OS_PRIO.C to allow them to be optimized in assembly language by replacing OS_PRIO.C with an assembly language equivalent OS_PRIO.ASM, when necessary. Function Description OS_PrioGetHighest() Find the highest priority level OS_PrioInsert() Set bit corresponding to priority level in the bitmap table OS_PrioRemove() Clear bit corresponding to priority level in the bitmap table Table 6-1 Priority Level access functions To determine the highest priority level that contains ready-to-run tasks, the bitmap table is scanned until the first bit set in the lowest bit position is found using OS_PrioGetHighest(). The code for this function is shown in Listing 6-1. OS_PRIO OS_PrioGetHighest (void) { CPU_DATA *p_tbl; OS_PRIO prio; prio = (OS_PRIO)0; p_tbl = &OSPrioTbl[0]; while (*p_tbl == (CPU_DATA)0) { prio += sizeof(CPU_DATA) * 8u; p_tbl++; } prio += (OS_PRIO)CPU_CntLeadZeros(*p_tbl); return (prio); (1) (2) (3) } Listing 6-1 Finding the highest priority level L6-1(1) 126 OS_PrioGetHighest() scans the table from OSPrioTbl[] until a non-zero entry is found. The loop will always terminate because there will always be a non-zero entry in the table because of the idle task. The Ready List L6-1(2) Each time a zero entry is found, we move to the next table entry and increment “prio” by the width (in number of bits) of each entry. If each entry is 32-bits wide, “prio” is incremented by 32. L6-1(3) Once the first non-zero entry is found, the number of “leading zeros” of that entry is simply added and return the priority level back to the caller. Counting the number of zeros is a CPU-specific function so that if a particular CPU has a built-in CLZ instruction, it is up to the implementer of the CPU port to take advantage of this feature. If the CPU used does not provide that instruction, the functionality must be implemented in C. The function CPU_CntLeadZeros() simply counts how many zeros there are in a CPU_DATA entry starting from the left (i.e., most significant bit). For example, assuming 32 bits, 0xF0001234 results in 0 leading zeros and 0x00F01234 results in 8 leading zeros. At first view, the linear path through the table might seem inefficient. However, if the number of priority levels is kept low, the search is quite fast. In fact, there are several optimizations to streamline the search. For example, if using a 32-bit processor and you are satisfied with limiting the number of different priority levels to 64, the above code can be optimized as shown in Listing 6-2. In fact, some processors have built-in “Count Leading Zeros” instructions and thus, the code can be written with just a few lines of assembly language. Remember that with μC/OS-III, 64 priority levels does not mean that the user is limited to 64 tasks since with μC/OS-III, any number of tasks are possible at a given priority level. OS_PRIO OS_PrioGetHighest (void) { OS_PRIO prio; if (OSPrioTbl[0] != (OS_PRIO_BITMAP)0) { prio = OS_CntLeadingZeros(OSPrioTbl[0]); } else { prio = OS_CntLeadingZeros(OSPrioTbl[1]) + 32; } return (prio); } Listing 6-2 Finding the highest priority level within 64 levels 127 Chapter 6 6-2 THE READY LIST Tasks that are ready to run are placed in the Ready List. As shown in Figure 6-1, the ready list is an array (OSRdyList[]) containing OS_CFG_PRIO_MAX entries, with each entry defined by the data type OS_RDY_LIST (see OS.H). An OS_RDY_LIST entry consists of three fields: .Entries, .TailPtr and .HeadPtr. .Entries contains the number of ready-to-run tasks at the priority level corresponding to the entry in the ready list. .Entries is set to zero (0) if there are no tasks ready to run at a given priority level. .TailPtr and .HeadPtr are used to create a doubly linked list of all the tasks that are ready at a specific priority. .HeadPtr points to the head of the list and .TailPtr points to its tail. The “index” into the array is the priority level associated with a task. For example, if a task is created at priority level 5 then it will be inserted in the table at OSRdyList[5] if that task is ready to run. Table 6-2 shows the functions that μC/OS-III uses to manipulate entries in the ready list. These functions are internal to μC/OS-III and the application code must never call them. Function Description OS_RdyListInit() Initialize the ready list to “empty” (see Figure 6-4) OS_RdyListInsert() Insert a TCB into the ready list OS_RdyListInsertHead() Insert a TCB at the head of the list OS_RdyListInsertTail() Insert a TCB at the tail of the list OS_RdyListMoveHeadToTail() Move a TCB from the head to the tail of the list OS_RdyListRemove() Remove a TCB from the ready list Table 6-2 Ready List access functions 128 The Ready List 26B5'26B&)*B35,2B0$;@ >@ >@ >@ >@ (QWULHV  (QWULHV  (QWULHV  (QWULHV  >@ (QWULHV  >@ (QWULHV  >26B&)*B35,2B0$;@ (QWULHV  7DLO3WU +HDG3WU 7DLO3WU +HDG3WU 7DLO3WU +HDG3WU 7DLO3WU +HDG3WU 7DLO3WU +HDG3WU 7DLO3WU +HDG3WU 7DLO3WU +HDG3WU               Figure 6-4 Empty Ready List Assuming all internal μC/OS-III’s tasks are enabled, Figure 6-5 shows the state of the ready list after calling OSInit() (i.e., μC/OS-III’s initialization). It is assumed that each μC/OS-III task had a unique priority. With μC/OS-III, this does not have to be the case. F6-4(1) There is only one entry in OSRdyList[OS_CFG_PRIO_MAX-1], the idle task. F6-4(2) The list points to OS_TCBs. Only relevant fields of the TCB are shown. The .PrevPtr and .NextPtr are used to form a doubly linked list of OS_TCBs associated to tasks at the same priority. For the idle task, these fields always point to NULL. F6-4(3) Priority 0 is reserved to the ISR handler task when OS_CFG_ISR_DEFERRED_EN is set to 1 in OS_CFG.H. In this case, this is the only task that can run at priority 0. 129 Chapter 6 26B5'26B35,2B0$;@ 267DVN,QW47&% >@ (QWULHV  7DLO3WU 3UHY3WU 1H[W3WU +HDG3WU 2WKHU)LHOGV    267LFN7DVN7&% >26B&)*B7,&.B7$6.B35,2@ (QWULHV   7DLO3WU 3UHY3WU 1H[W3WU +HDG3WU 2WKHU)LHOGV   267PU7DVN7&% >26B&)*B705B7$6.B35,2@  (QWULHV  3UHY3WU 1H[W3WU 7DLO3WU 2WKHU)LHOGV +HDG3WU   266WDW7DVN7&% >26B&)*B67$7B7$6.B35,2@  (QWULHV  3UHY3WU 1H[W3WU 7DLO3WU 2WKHU)LHOGV +HDG3WU   >26B35,2B0$;@ (QWULHV  26,GOH7DVN7&% 3UHY3WU 1H[W3WU 7DLO3WU 2WKHU)LHOGV +HDG3WU      Figure 6-5 Ready List after calling OSInit() F6-5(1) The tick task and the other two optional tasks have their own priority level, as shown. μC/OS-III enables the user to have multiple tasks at the same priority and thus, the tasks could be set up as shown. Typically, one would set the priority of the tick task higher than the timer task and, the timer task higher in priority than the statistic task. F6-5(2) Both the tail and head pointers point to the same TCB when there is only one TCB at a given priority level. 130 The Ready List 6-3 ADDING TASKS TO THE READY LIST Tasks are added to the ready list by a number of μC/OS-III services. The most obvious service is OSTaskCreate(), which always creates a task in the ready-to-run state and adds the task to the ready list. As shown in Figure 6-6, when creating a task, and specifying a priority level where tasks already exist (two in this example) in the ready list at that priority level, OSTaskCreate() will insert the new task at the end of the list of tasks at that priority level.  26B5'@ >SULR@ (QWULHV  3UHY3WU 1H[W3WU +HDG3WU 2WKHU)LHOGV 7DLO3WU  %()25( 3UHY3WU 1H[W3WU 2WKHU)LHOGV   26B5'@ >SULR@ (QWULHV  3UHY3WU 1H[W3WU +HDG3WU 2WKHU)LHOGV 7DLO3WU  3UHY3WU 1H[W3WU 2WKHU)LHOGV $)7(5     3UHY3WU 1H[W3WU 2WKHU)LHOGV   Figure 6-6 Inserting a newly created task in the ready list F6-6(1) Before calling OSTaskCreate() (in this example), two tasks were in the ready list at priority “prio”. F6-6(2) A new TCB is passed to OSTaskCreate() and, μC/OS-III initialized the contents of that TCB. 131 Chapter 6 F6-6(3) OSTaskCreate() calls OS_RdyListInsertTail(), which links the new TCB to the ready list by setting up four pointers and also incrementing the .Entries field of OSRdyList[prio]. Not shown in Figure 6-6 is that OSTaskCreate() also calls OS_PrioInsert() to set the bit in the bitmap table. Of course, this operation is not necessary as there are already entries in the list at this priority. However, OS_PrioInsert() is a very fast call and thus it should not affect performance. The reason the new TCB is added to the end of the list is that the current head of the list could be the task creator and, at the same priority, there is no reason to make the new task the next task to run. In fact, a task being made ready will be inserted at the tail of the list if the current task is at the same priority. However, if a task is being made ready at a different priority than the current task, it will be inserted at the head of the list. 6-4 SUMMARY μC/OS-III supports any number of different priority levels. However, 256 different priority levels should be sufficient for the most complex applications and most systems will not require more than 64 levels. The ready list consist of two data structures: a bitmap table that keeps track of which priority level is ready, and a table containing a list of all the tasks ready at each priority level. Processors having “count leading zeros” instructions can accelerate the table lookup process used in determining the highest priority task. 132 Chapter 7 Scheduling The scheduler, also called the dispatcher, is a part of μC/OS-III responsible for determining which task runs next. μC/OS-III is a preemptive, priority-based kernel. Each task is assigned a priority based on its importance. The priority for each task depends on the application, and μC/OS-III supports multiple tasks at the same priority level. The word preemptive means that when an event occurs, and that event makes a more important task ready to run, then μC/OS-III will immediately give control of the CPU to that task. Thus, when a task signals or sends a message to a higher-priority task, the current task is suspended and the higher-priority task is given control of the CPU. Similarly, if an Interrupt Service Routine (ISR) signals or sends a message to a higher priority task, when the message is completed, the interrupted task remains suspended, and the new higher priority task resumes. 133 Chapter 7 7-1 PREEMPTIVE SCHEDULING μC/OS-III handles event posting from interrupts using two different methods: Direct and Deferred Post. These will be discussed in greater detail in Chapter 9, “Interrupt Management” on page 157. From a scheduling point of view, the end result of the two methods is the same; the highest priority and ready task will receive the CPU as shown in Figures 6-1 and 6-2.  ,65 ,65   —&26,,,  +LJK3ULRULW\ 7DVN /RZ3ULRULW\ 7DVN    7DVN     7DVN 7DVN Figure 7-1 Preemptive scheduling – Direct Method F7-1(1) A low priority task is executing, and an interrupt occurs. F7-1(2) If interrupts are enabled, the CPU vectors (i.e., jumps) to the ISR that is responsible for servicing the interrupting device. F7-1(3) The ISR services the device and signals or sends a message to a higher-priority task waiting to service this device. This task is thus ready to run. F7-1(4) When the ISR completes its work it makes a service call to μC/OS-III. F7-1(5) F7-1(6) 134 Since there is a more important ready-to-run task, μC/OS-III decides to not return to the interrupted task but switches to the more important task. See Chapter 8, “Context Switching” on page 147 for details on how this works. Scheduling F7-1(7) F7-1(8) F7-1(9) F7-1(10) F7-1(11) The higher priority task services the interrupting device and, when finished, calls μC/OS-III asking it to wait for another interrupt from the device. μC/OS-III blocks the high-priority task until the next device interrupts. Since the device has not interrupted a second time, μC/OS-III switches back to the original task (the one that was interrupted). The interrupted task resumes execution, exactly at the point where it was interrupted. Figure 7-2 shows that μC/OS-III performs a few extra steps when it is configured for the Deferred Post method. Notice that the end results is the same; the high-priority task preempts the low-priority one.  ,65 ,65   —&26,,,  —&26,,, ,65 +DQGOHU   +LJK3ULRULW\ 7DVN /RZ3ULRULW\ 7DVN 7DVN 7DVN 7DVN Figure 7-2 Preemptive scheduling – Deferred Post Method 135 Chapter 7 F7-2(1) The ISR services the device and, instead of signaling or sending the message to the task, μC/OS-III (through the POST call) places the post call into a special queue and makes a very high-priority task (actually the highest-possible priority) ready to run. This task is called the ISR Handler Task. F7-2(2) When the ISR completes its work, it makes a service call to μC/OS-III. F7-2(3) F7-2(4) F7-2(5) F7-2(6) Since the ISR made the ISR Handler Task ready to run, μC/OS-III switches to that task. The ISR Handler Task then removes the post call from the message queue and reissues the post. This time, however, it does it at the task level instead of the ISR level. The reason this extra step is performed is to keep interrupt disable time as small as possible. See Chapter 9, “Interrupt Management” on page 157 to find out more on the subject. When the queue is emptied, μC/OS-III removes the ISR Handler Task from the ready list and switches to the task that was signaled or sent a message. 7-2 SCHEDULING POINTS Scheduling occurs at scheduling points and nothing special must be done in the application code since scheduling occurs automatically based on the conditions described below. A task signals or sends a message to another task: This occurs when the task signaling or sending the message calls one of the post services, OS???Post(). Scheduling occurs towards the end of the OS???Post() call. Note that scheduling does not occur if one specifies (as part of the post call) to not invoke the scheduler (i.e., set the option argument to OS_OPT_POST_NO_SCHED). A task calls OSTimeDly() or OSTimeDlyHMSM(): If the delay is non-zero, scheduling always occurs since the calling task is placed in a list waiting for time to expire. Scheduling occurs as soon as the task is inserted in the wait list. 136 Scheduling A task waits for an event to occur and the event has not yet occurred: This occurs when one of the OS???Pend() functions are called. The task is placed in the wait list for the event and, if a non-zero timeout is specified, then the task is also inserted in the list of tasks waiting to timeout. The scheduler is then called to select the next most important task to run. If a task aborts a pend: A task is able to abort the wait (i.e., pend) of another task by calling OS???PendAbort(). Scheduling occurs when the task is removed from the wait list for the specified kernel object. If a task is created: The newly created task may have a higher priority than the task’s creator. In this case, the scheduler is called. If a task is deleted: When terminating a task, the scheduler is called if the current task is deleted. If a kernel object is deleted: If you delete an event flag group, a semaphore, a message queue, or a mutual exclusion semaphore, if tasks are waiting on the kernel object, those tasks will be made ready to run and the scheduler will be called to determine if any of the tasks have a higher priority than the task that deleted the kernel object. A task changes the priority of itself or another task: The scheduler is called when a task changes the priority of another task (or itself) and the new priority of that task is higher than the task that changed the priority. A task suspends itself by calling OSTaskSuspend(): The scheduler is called since the task that called OSTaskSuspend() is no longer able to execute, and must be resumed by another task. A task resumes another task that was suspended by OSTaskSuspend(): The scheduler is called if the resumed task has a higher priority than the task that calls OSTaskResume(). 137 Chapter 7 At the end of all nested ISRs: The scheduler is called at the end of all nested ISRs to determine whether a more important task is made ready to run by one of the ISRs. The scheduling is actually performed by OSIntExit() instead of OSSched(). The scheduler is unlocked by calling OSSchedUnlock(): The scheduler is unlocked after being locked. Lock the scheduler by calling OSSchedLock(). Note that locking the scheduler can be nested and the scheduler must be unlocked a number of times equal to the number of locks. A task gives up its time quanta by calling OSSchedRoundRobinYield(): This assumes that the task is running alongside with other tasks at the same priority and the currently running task decides that it can give up its time quanta and let another task run. The user calls OSSched(): The application code can call OSSched() to run the scheduler. This only makes sense if calling OS???Post() functions and specifying OS_OPT_POST_NO_SCHED so that multiple posts can be accomplished without running the scheduler on every post. Of course, in the above situation, the last post can be a post without the OS_OPT_POST_NO_SCHED option. 7-3 ROUND-ROBIN SCHEDULING When two or more tasks have the same priority, μC/OS-III allows one task to run for a predetermined amount of time (called a Time Quanta) before selecting another task. This process is called Round-Robin Scheduling or Time Slicing. If a task does not need to use its full time quanta it can voluntarily give up the CPU so that the next task can execute. This is called Yielding. μC/OS-III allows the user to enable or disable round robin scheduling at run time. 138 Scheduling Figure 7-3 shows a timing diagram with tasks running at the same priority. There are three tasks that are ready to run at priority “X”. For sake of illustration, the time quanta occurs every 4th clock tick. This is shown as a darker tick mark. 7LPH 4XDQWD 7LFN ,65   —&26,,, 7DVN 3ULRULW\;   7DVN 7DVN  7DVN 3ULRULW\; 7DVN 3ULRULW\;  7DVN 7DVN   7DVN 7DVN 7DVN Figure 7-3 Round Robin Scheduling F7-3(1) Task #3 is executing. During that time, tick interrupts occur but the time quanta have not expired yet for Task #3. F7-3(2) On the 4th tick interrupt, the time quanta for Task #3 expire. F7-3(3) μC/OS-III resumes Task #1 since it was the next task in the list of tasks at priority “X” that was ready to run. F7-3(4) Task #1 executes until its time quanta expires (i.e., after four ticks). 139 Chapter 7 F7-3(5) F7-3(6) F7-3(7) F7-3(8) Here Task #3 executes but decides to give up its time quanta by calling the μC/OS-III function OSSchedRoundRobinYield(), which causes the next task in the list of tasks ready at priority “X” to execute. An interesting thing occurred when μC/OS-III scheduled Task #1. It reset the time quanta for that task to four ticks so that the next time quanta will expire four ticks from this point. Task #1 executes for its full time quanta. μC/OS-III allows the user to change the time quanta at run time through the OSSchedRoundRobinCfg() function (see Appendix A, “μC/OS-III API Reference Manual” on page 375). This function also allows round robin scheduling to be enabled/disabled, and the ability to change the default time quanta. μC/OS-III also enables the user to specify the time quanta on a per-task basis. One task could have a time quanta of 1 tick, another 12, another 3, and yet another 7, etc. The time quanta of a task is specified when the task is created. The time quanta of a task may also be changed at run time through the function OSTaskTimeQuantaSet(). 140 Scheduling 7-4 SCHEDULING INTERNALS Scheduling is performed by two functions: OSSched() and OSIntExit(). OSSched() is called by task level code while OSIntExit() is called by ISRs. Both functions are found in OS_CORE.C. Figure 7-1 illustrates the two sets of data structures that the scheduler uses; the priority ready bitmap and the ready list as described in Chapter 6, “The Ready List” on page 123. OSPrioTbl[] OSRdyList [] [0] [1] [2] [3] [4] [OS_CFG_PRIO_MAX-2] Idle Task [OS_CFG_PRIO_MAX-1] OS_TCBs Figure 7-4 Priority ready bitmap and Ready list 141 Chapter 7 7-4-1 OSSched() The pseudo code for the task level scheduler, OSSched() is shown in Listing 7-1. void OSSched (void) { Disable interrupts; if (OSIntNestingCtr > 0) { return; } if (OSSchedLockNestingCtr > 0) { return; } Get highest priority ready; Get pointer to OS_TCB of next highest priority task; if (OSTCBNHighRdyPtr != OSTCBCurPtr) { Perform task level context switch; } Enable interrupts; (1) (2) (3) (4) (5) } Listing 7-1 OSSched() pseudocode L7-1(1) OSSched() starts by making sure it is not called from an ISR as OSSched() is the task level scheduler. Instead, an ISR must call OSIntExit(). If OSSched() is called by an ISR, OSSched() simply returns. L7-1(2) The next step is to make sure the scheduler is not locked. If the code calls OSSchedLock() the user does not want to run the scheduler and the function just returns. L7-1(3) OSSched() determines the priority of the highest priority task ready by scanning the bitmap OSPrioTbl[] as described in Chapter 6, “The Ready List” on page 123. L7-1(4) Once it is known which priority is ready, index into the OSRdyList[] and extract the OS_TCB at the head of the list (i.e., OSRdyList[highest priority].HeadPtr). At this point, it is known which OS_TCB to switch to and which OS_TCB to save to as this was the task that called OSSched(). Specifically, OSTCBCurPtr points to the current task’s OS_TCB and OSTCBHighRdyPtr points to the new OS_TCB to switch to. 142 Scheduling L7-1(5) If the user is not attempting to switch to the same task that is currently running, OSSched() calls the code that will perform the context switch (see Chapter 8, “Context Switching” on page 147). As the code indicates, however, the task level scheduler calls a task-level function to perform the context switch. Notice that the scheduler and the context switch runs with interrupts disabled. This is necessary because this process needs to be atomic. 7-4-2 OSIntExit() The pseudo code for the ISR level scheduler, OSIntExit() is shown in Listing 7-2. Note that interrupts are assumed to be disabled when OSIntExit() is called. void OSIntExit (void) { if (OSIntNestingCtr == 0) { return; } OSIntNestingCtr--; if (OSIntNestingCtr > 0) { return; } if (OSSchedLockNestingCtr > 0) { return; } Get highest priority ready; Get pointer to OS_TCB of next highest priority task; if (OSTCBHighRdyPtr != OSTCBCurPtr) { Perform ISR level context switch; } } (1) (2) (3) (4) (5) (6) Listing 7-2 OSIntExit() pseudocode L7-2(1) OSIntExit() starts by making sure that the call to OSIntExit() will not cause OSIntNestingCtr to wrap around. This would be extremely and unlikely occurrence, but not worth taking a chance that it might. L7-2(2) OSIntExit() decrements the nesting counter as OSIntExit() is called at the end of an ISR. If all ISRs have not nested, the code simply returns. There is no need to run the scheduler since there are still interrupts to return to. 143 Chapter 7 L7-2(3) OSIntExit() checks to see that the scheduler is not locked. If it is, OSIntExit() does not run the scheduler and simply returns to the interrupted task that locked the scheduler. L7-2(4) Finally, this is the last nested ISR (we are returning to task-level code) and the scheduler is not locked. Therefore, we need to find the highest priority task that needs to run. L7-2(5) Again, we extract the highest priority OS_TCB from OSRdyList[]. L7-2(6) If the highest-priority task is not the current task μC/OS-III performs an ISR level context switch. The ISR level context switch is different as it is assumed that the interrupted task’s context was saved at the beginning of the ISR and it is only left to restore the context of the new task to run. This is described in Chapter 8, “Context Switching” on page 147. 7-4-3 OS_SchedRoundRobin() When the time quanta for a task expires and there are multiple tasks at the same priority, μC/OS-III will select and run the next task that is ready to run at the current priority. OS_SchedRoundRobin() is the code used to perform this operation. OS_SchedRoundRobin() is either called by OSTimeTick() or OS_IntQTask(). OS_SchedRoundRobin() is found in OS_CORE.C. OS_SchedRoundRobin() is called by OSTimeTick() when you selected the Direct Method of posting (see Chapter 9, “Interrupt Management” on page 157). OS_SchedRoundRobin() is called by OS_IntQTask() when selecting the Deferred Post Method of posting, described in Chapter 8. The pseudo code for the round-robin scheduler is shown in Listing 7-3. 144 Scheduling void OS_SchedRoundRobin (void) { if (OSSchedRoundRobinEn != TRUE) { return; } if (Time quanta counter > 0) { Decrement time quanta counter; } if (Time quanta counter > 0) { return; } if (Number of OS_TCB at current priority level < 2) { return; } if (OSSchedLockNestingCtr > 0) { return; } Move OS_TCB from head of list to tail of list; Reload time quanta for current task; (1) (2) (3) (4) (5) (6) } Listing 7-3 OS_SchedRoundRobin() pseudocode L7-3(1) OS_SchedRoundRobin() starts by making sure that round robin scheduling is enabled. Recall that to enable round robin scheduling, one must call OSSchedRoundRobinCfg(). L7-3(2) The time quanta counter, which resides inside the OS_TCB of the running task, is decremented. If the value is still non-zero then OS_SchedRoundRobin() returns. L7-3(3) Once the time quanta counter reaches zero, check to see that there are other ready-to-run tasks at the current priority. If there are none, return. Round robin scheduling only applies when there are multiple tasks at the same priority and the task doesn’t completes its work within its time quanta. L7-3(4) OS_SchedRoundRobin() also returns if the scheduler is locked. L7-3(5) Next, OS_SchedRoundRobin() move the OS_TCB of the current task from the head of the ready list to the end. 145 Chapter 7 L7-3(6) The time quanta for the task at the head of the list are loaded. Each task may specify its own time quanta when the task is created or through OSTaskTimeQuantaSet(). If specifying 0, μC/OS-III assumes the default time quanta, which corresponds to the value in the variable OSSchedRoundRobinDfltTimeQuanta. 7-5 SUMMARY μC/OS-III is a preemptive scheduler so it will always execute the highest priority task that is ready to run. μC/OS-III allows for multiple tasks at the same priority. If there are multiple ready-to-run tasks, μC/OS-III will round robin between these tasks. Scheduling occurs at specific scheduling points, when the application calls μC/OS-III functions. μC/OS-III has two schedulers: OSSched(), which is called by task-level code, and OSIntExit() called at the end of each ISR. 146 Chapter 8 Context Switching When μC/OS-III decides to run a different task (see Chapter 7, “Scheduling” on page 133), it saves the current task’s context, which typically consists of the CPU registers, onto the current task’s stack and restores the context of the new task and resumes execution of that task. This process is called a Context Switch. Context switching adds overhead. The more registers a CPU has, the higher the overhead. The time required to perform a context switch is generally determined by how many registers must be saved and restored by the CPU. The context switch code is generally part of a processor’s port of μC/OS-III. A port is the code needed to adapt μC/OS-III to the desired processor. This code is placed in special C and assembly language files: OS_CPU.H, OS_CPU_C.C and OS_CPU_A.ASM. Chapter 18, “Porting μC/OS-III” on page 335, Porting μC/OS-III provides more details on the steps needed to port μC/OS-III to different CPU architectures. In this chapter, we will discuss the context switching process in generic terms using a fictitious CPU as shown in Figure 8-1. Our fictitious CPU contains 16 integer registers (R0 to R15), a separate ISR stack pointer, and a separate status register (SR). Every register is 32 bits wide and each of the 16 integer registers can hold either data or an address. The program counter (or instruction pointer) is R15 and there are two separate stack pointers labeled R14 and R14”. R14 represents a task stack pointer (TSP), and R14” represents an ISR stack pointer (ISP). The CPU automatically switches to the ISR stack when servicing an exception or interrupt. The task stack is accessible from an ISR (i.e., we can push and pop elements onto the task stack when in an ISR), and the interrupt stack is also accessible from a task. 147 Chapter 8 ELW 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 7DVN6WDFN3RLQWHU 763 ,65 6WDFN3RLQWHU ,63 3URJUDP&RXQWHU 65 Figure 8-1 Fictitious CPU In μC/OS-III, the stack frame for a ready task is always setup to look as if an interrupt has just occurred and all processor registers were saved onto it. Tasks enter the ready state upon creation and thus their stack frames are pre-initialized by software in a similar manner. Using our fictitious CPU, we’ll assume that a stack frame for a task that is ready to be restored is shown in Figure 8-2. The task stack pointer points to the last register saved onto the task’s stack. The program counter and status registers are the first registers saved onto the stack. In fact, these are saved automatically by the CPU when an exception or interrupt occurs (assuming interrupts are enabled) while the other registers are pushed onto the stack by software in the exception handler. The stack pointer (R14) is not actually saved on the stack but instead is saved in the task’s OS_TCB. 148 Context Switching The interrupt stack pointer points to the current top-of-stack for the interrupt stack, which is a different memory area. When an ISR executes, the processor uses R14” as the stack pointer for function calls and local arguments. /RZ 0HPRU\ $GGUHVV 5 5 5 5 7DVN 6WDFN 3RLQWHU 763 5 5 5 5 5 5 5 5 ,QWHUUXSW 6WDFN 3RLQWHU ,63 7RS2I ,QWHUUXSW 6WDFN 5 5 5 5 +LJK 0HPRU\ $GGUHVV 5 3URJUDP&RXQWHU 65 6WDWXV5HJLVWHU Figure 8-2 CPU register stacking order of ready task There are two types of context switches: one performed from a task and another from an ISR. The task level context switch is implemented by the code in OSCtxSw(), which is actually invoked by the macro OS_TASK_SW(). A macro is used as there are many ways to invoke OSCtxSw() such as software interrupts, trap instructions, or simply calling the function. The ISR context switch is implemented by OSIntCtxSw(). The code for both functions is typically written in assembly language and is found in a file called OS_CPU_A.ASM. 149 Chapter 8 8-1 OSCtxSw() OSCtxSw() is called when the task level scheduler (OSSched()) determines that a new high priority task needs to execute. Figure 8-3 shows the state of several μC/OS-III variables and data structures just prior to calling OSCtxSw(). 26B7&% 26B7&% 5$0 6WN3WU 5$0 267&%+LJK5G\3WU 267&%&XU3WU  6WN3WU   &38 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5  5$0  /RZ 0HPRU\ $GGUHVV 5 5 5 5 5 5 5 5 5 5 5 5 5 5 763 5 3& 5 3& 65 65 5$0 +LJK 0HPRU\ $GGUHVV Figure 8-3 Variables and data structures prior to calling OSCtxSw() F8-3(1) OSTCBCurPtr points to the OS_TCB of the task that is currently running and that called OSSched(). F8-3(2) OSSched() finds the new task to run by having OSTCBHighRdyPtr point to its OS_TCB. 150 Context Switching F8-3(3) OSTCBHighRdyPtr->StkPtr points to the top of stack of the new task to run. F8-3(4) When μC/OS-III creates or suspends a task, it always leaves the stack frame to look as if an interrupt just occurred and all the registers saved onto it. This represents the expected state of the task so it can be resumed. F8-3(5) The CPU’s stack pointer points within the stack area (i.e., RAM) of the task that called OSSched(). Depending on how OSCtxSw() is invoked, the stack pointer may be pointing at the return address of OSCtxSw(). Figure 8-4 shows the steps involved in performing the context switch as implemented by OSCtxSw(). 6WN3WU 267&%+LJK5G\3WU 267&%&XU3WU 6WN3WU  /RZ 0HPRU\ $GGUHVV  &38 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 3& 5 763 5 3& 65 5 3& 65 5$0 65 5$0 5 +LJK 0HPRU\ $GGUHVV  /RZ 0HPRU\ $GGUHVV  +LJK 0HPRU\ $GGUHVV Figure 8-4 Operations performed by OSCtxSw() 151 Chapter 8 F8-4(1) OSCtxSw() begins by saving the status register and program counter of the current task onto the current task’s stack. The saving order of register depends on how the CPU expects the registers on the stack frame when an interrupt occurs. In this case, it is assumed that the SR is stacked first. The remaining registers are then saved onto the stack. F8-4(2) OSCtxSw() saves the contents of the CPU’s stack pointer into the OS_TCB of the task being suspended. In other words, OSTCBCurPtr->StkPtr = R14. F8-4(3) OSCtxSw() then loads the CPU stack pointer with the saved top-of-stack from the new task’s OS_TCB. In other words, R14 = OSTCBHighRdyPtr->StkPtr. F8-4(4) Finally, OSCtxSw() retrieves the CPU register contents from the new stack. The program counter and status registers are generally retrieved at the same time by executing a return from interrupt instruction. 152 Context Switching 8-2 OSIntCtxSw() OSIntCtxSw() is called when the ISR level scheduler (OSIntExit()) determines that a new high priority task is ready to execute. Figure 8-5 shows the state of several μC/OS-III variables and data structures just prior to calling OSIntCtxSw(). 6WN3WU /RZ 0HPRU\ $GGUHVV +LJK 0HPRU\ $GGUHVV 267&%+LJK5G\3WU 267&%&XU3WU 6WN3WU &38 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 3& 5 763 5 3& 65 5 3& 65 5$0 65 5$0 /RZ 0HPRU\ $GGUHVV +LJK 0HPRU\ $GGUHVV Figure 8-5 Variables and data structures prior to calling OSIntCtxSw() μC/OS-III assumes that CPU registers are saved onto the task’s stack at the beginning of an ISR (see Chapter 9, “Interrupt Management” on page 157). Because of this, notice that OSTCBCurPtr->StkPtr contains a pointer to the top-of-stack pointer of the task being suspended (the one on the left). OSIntCtxSw() does not have to worry about saving the CPU registers of the suspended task since that is already finished. 153 Chapter 8 Figure 8-6 shows the operations performed by OSIntCtxSw() to complete the second half of the context switch. This is exactly the same process as the second half of OSCtxSw(). 6WN3WU 267&%+LJK5G\3WU 267&%&XU3WU 6WN3WU  /RZ 0HPRU\ $GGUHVV +LJK 0HPRU\ $GGUHVV &38 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 3& 5 763 5 3& 65 5 3& 65 5$0 65 5$0 /RZ 0HPRU\ $GGUHVV  +LJK 0HPRU\ $GGUHVV Figure 8-6 Operations performed by OSIntCtxSw() F8-6(1) OSIntCtxSw() loads the CPU stack pointer with the saved top-of-stack from the new task’s OS_TCB. R14 = OSTCBHighRdyPtr->StkPtr. F8-6(2) OSIntCtxSw() then retrieves the CPU register contents from the new stack. The program counter and status registers are generally retrieved at the same time by executing a return from interrupt instruction. 154 Context Switching 8-3 SUMMARY A context switch consists of saving the context (i.e., CPU registers) associated with one task and restoring the context of a new, higher-priority task. The new task to be switched to is determined by OSSched() when a context switch is initiated by task level code, and OSIntExit() when initiated by an ISR. OSCtxSw() performs the context switch for OSSched() and OSIntCtxSw() performs the context switch for OSIntExit(). However, OSIntCtxSw() only needs to perform the second half of the context switch because it is assumed that the ISR saved CPU registers upon entry to the ISR. 155 Chapter 8 156 Chapter 9 Interrupt Management An interrupt is a hardware mechanism used to inform the CPU that an asynchronous event occurred. When an interrupt is recognized, the CPU saves part (or all) of its context (i.e., registers) and jumps to a special subroutine called an Interrupt Service Routine (ISR). The ISR processes the event, and – upon completion of the ISR – the program either returns to the interrupted task, or the highest priority task, if the ISR made a higher priority task ready to run. Interrupts allow a microprocessor to process events when they occur (i.e., asynchronously), which prevents the microprocessor from continuously polling (looking at) an event to see if it occurred. Task level response to events is typically better using interrupt mode as opposed to polling mode, however at the possible cost of increased interrupt latency. Microprocessors allow interrupts to be ignored or recognized through the use of two special instructions: disable interrupts and enable interrupts, respectively. In a real-time environment, interrupts should be disabled as little as possible. Disabling interrupts affects interrupt latency possibly causing interrupts to be missed. Processors generally allow interrupts to be nested, which means that while servicing an interrupt, the processor recognizes and services other (more important) interrupts. One of the most important specifications of a real-time kernel is the maximum amount of time that interrupts are disabled. This is called interrupt disable time. All real-time systems disable interrupts to manipulate critical sections of code and re-enable interrupts when critical sections are completed. The longer interrupts are disabled, the higher the interrupt latency. Interrupt response is defined as the time between the reception of the interrupt and the start of the user code that handles the interrupt. Interrupt response time accounts for the entire overhead involved in handling an interrupt. Typically, the processor’s context (CPU registers) is saved on the stack before the user code is executed. 157 Chapter 9 Interrupt recovery is defined as the time required for the processor to return to the interrupted code or to a higher priority task if the ISR made such a task ready to run. Task latency is defined as the time it takes from the time the interrupt occurs to the time task level code resumes. 9-1 HANDLING CPU INTERRUPTS There are many popular CPU architectures on the market today, and most processors typically handle interrupts from a multitude of sources. For example, a UART receives a character, an Ethernet controller receives a packet, a DMA controller completes a data transfer, an Analog-to-Digital Converter (ADC) completes an analog conversion, a timer expires, etc. In most cases, an interrupt controller captures all of the different interrupts presented to the processor as shown in Figure 9-1 (note that the “CPU Interrupt Enable/Disable” is typically part of the CPU, but is shown here separately for sake of the illustration). Interrupting devices signal the interrupt controller, which then prioritizes the interrupts and presents the highest-priority interrupt to the CPU. Device Interrupt Device Interrupt Disable Interrupt Controller Device Interrupt Device Interrupt Interrupt CPU Enable CPU Interrupt Enable/Disable Figure 9-1 Interrupt controllers Modern interrupt controllers have built-in intelligence that enable the user to prioritize interrupts, remember which interrupts are still pending and, in many cases, have the interrupt controller provide the address of the ISR (also called the vector address) directly to the CPU. 158 Interrupt Management If “global” interrupts (i.e., the switch in Figure 9-1) are disabled, the CPU will ignore requests from the interrupt controller, but they will be held pending by the interrupt controller until the CPU re-enables interrupts. CPUs deal with interrupts using one of two models: 1 All interrupts vector to a single interrupt handler. 2 Each interrupt vectors directly to an interrupt handler. Before discussing these two methods, it is important to understand how μC/OS-III handles CPU interrupts. 9-2 TYPICAL μC/OS-III INTERRUPT SERVICE ROUTINE (ISR) μC/OS-III requires that an interrupt service routine be written in assembly language. However, if a C compiler supports in-line assembly language, the ISR code can be placed directly into a C source file. The pseudo-code for a typical ISR when using μC/OS-III is shown in Listing 9-1. MyISR: (1) Disable all interrupts; (2) Save the CPU registers; (3) OSIntNestingCtr++; (4) if (OSIntNestingCtr == 1) { (5) OSTCBCurPtr->StkPtr = Current task’s CPU stack pointer register value; } Clear interrupting device; (6) Re-enable interrupts (optional); (7) Call user ISR; (8) OSIntExit(); (9) Restore the CPU registers; (10) Return from interrupt; (11) Listing 9-1 ISRs under μC/OS-III (assembly language) 159 Chapter 9 L9-1(1) As mentioned above, an ISR is typically written in assembly language. MyISR corresponds to the name of the handler that will handle the interrupting device. L9-1(2) It is important that all interrupts are disabled before going any further. Some processors have interrupts disabled whenever an interrupt handler starts. Others require the user to explicitly disable interrupts as shown here. This step may be tricky if a processor supports different interrupt priority levels. However, there is always a way to solve the problem. L9-1(3) The first thing the interrupt handler must do is save the context of the CPU onto the interrupted task’s stack. On some processors, this occurs automatically. However, on most processors it is important to know how to save the CPU registers onto the task’s stack. Save the full “context” of the CPU, which may also include Floating-Point Unit (FPU) registers if the CPU used is equipped with an FPU. Certain CPUs also automatically switch to a special stack just to process interrupts (i.e., an interrupt stack). This is generally beneficial as it avoids using up valuable task stack space. However, for μC/OS-III, the context of the interrupted task needs to be saved onto that task’s stack. If the processor does not have a dedicated stack pointer to handle ISRs then it is possible to implement one in software. Specifically, upon entering the ISR, simply save the current task stack, switch to a dedicated ISR stack, and when done with the ISR switch back to the task stack. Of course, this means that there is additional code to write, however the benefits are enormous since it is not necessary to allocate extra space on the task stacks to accommodate for worst case interrupt stack usage including interrupt nesting. L9-1(4) Next, either call OSIntEnter(), or simply increment the variable OSIntNestingCtr in assembly language. This is generally quite easy to do and is more efficient than calling OSIntEnter(). As its name implies, OSIntNestingCtr keeps track of the interrupt nesting level. L9-1(5) If this is the first nested interrupt, save the current value of the stack pointer of the interrupted task into its OS_TCB. The global pointer OSTCBCurPtr conveniently points to the interrupted task’s OS_TCB. The very first field in 160 Interrupt Management OS_TCB is where the stack pointer needs to be saved. In other words, OSTCBCurPtr->StkPtr happens to be at offset 0 in the OS_TCB (this greatly simplifies assembly language). L9-1(6) At this point, clear the interrupting device so that it does not generate another interrupt until it is ready to do so. The user does not want the device to generate the same interrupt if re-enabling interrupts (refer to the next step). However, most people defer the clearing of the source and prefer to perform the action within the user ISR handler in “C.” L9-1(7) At this point, it is safe to re-enable interrupts if the developer wants to support nested interrupts. This step is optional. L9-1(8) At this point, further processing can be deferred to a C function called from assembly language. This is especially useful if there is a large amount of processing to do in the ISR handler. However, as a general rule, keep the ISRs as short as possible. In fact, it is best to simply signal or send a message to a task and let the task handle the details of servicing the interrupting device. The ISR must call one of the following functions: OSSemPost(), OSTaskSemPost(), OSFlagPost(), OSQPost() or OSTaskQPost(). This is necessary since the ISR will notify a task, which will service the interrupting device. These are the only functions able to be called from an ISR and they are used to signal or send a message to a task. However, if the ISR does not need to call one of these functions, consider writing the ISR as a “Short Interrupt Service Routine,” as described in the next section. L9-1(9) When completing the ISR, the user must call OSIntExit() to tell μC/OS-III that the ISR has completed. OSIntExit() simply decrements OSIntNestingCtr and, if OSIntNestingCtr goes to 0, this indicates that the ISR will return to task-level code (instead of a previously interrupted ISR). μC/OS-III will need to determine whether there is a higher priority task that needs to run because of one of the nested ISRs. In other words, the ISR might have signaled or sent a message to a higher- priority task waiting for this signal or message. In this case, μC/OS-III will context switch to this higher priority task instead of returning to the interrupted task. In this latter case, OSIntExit() does not actually return, but takes a different path. 161 Chapter 9 L9-1(10) If the ISR signaled or sent a message to a lower-priority task than the interrupted task, OSIntExit() returns. This means that the interrupted task is still the highest-priority task to run and it is important to restore the previously saved registers. L9-1(11) The ISR performs a return from interrupts and so resumes the interrupted task. NOTE: From this point on, (1) to (6) will be referred to as the ISR Prologue and (9) to (11) as the ISR Epilogue. 9-3 SHORT INTERRUPT SERVICE ROUTINE (ISR) The above sequence assumes that the ISR signals or sends a message to a task. However, in many cases, the ISR may not need to notify a task and can simply perform all of its work within the ISR (assuming it can be done quickly). In this case, the ISR will appear as shown in Listing 9-2. MyShortISR: Save enough registers as needed by the ISR; Clear interrupting device; DO NOT re-enable interrupts; Call user ISR; Restore the saved CPU registers; Return from interrupt; (1) (2) (3) (4) (5) (6) (7) Listing 9-2 Short ISRs with μC/OS-III L9-2(1) As mentioned above, an ISR is typically written in assembly language. MyShortISR corresponds to the name of the handler that will handle the interrupting device. L9-2(2) Here, save sufficient registers as required to handle the ISR. L9-2(3) The user may want to clear the interrupting device to prevent it from generating the same interrupt once the ISR returns. 162 Interrupt Management L9-2(4) Do not re-enable interrupts at this point since another interrupt could make μC/OS-III calls, forcing a context switch to a higher-priority task. This means that the above ISR would complete, but at a much later time. L9-2(5) Now take care of the interrupting device in assembly language or call a C function, if necessary. L9-2(6) Once finished, simply restore the saved CPU registers. L9-2(7) Perform a return from interrupt to resume the interrupted task. Short ISRs, as described above, should be the exception and not the rule since μC/OS-III has no way of knowing when these ISRs occur. 9-4 ALL INTERRUPTS VECTOR TO A COMMON LOCATION Even though an interrupt controller is present in most designs, some CPUs still vector to a common interrupt handler, and an ISR queries the interrupt controller to determine the source of the interrupt. At first glance, this might seem silly since most interrupt controllers are able to force the CPU to jump directly to the proper interrupt handler. It turns out, however, that for μC/OS-III, it is easier to have the interrupt controller vector to a single ISR handler than to vector to a unique ISR handler for each source. Listing 9-3 describes the sequence of events to be performed when the interrupt controller forces the CPU to vector to a single location. An interrupt occurs; The CPU vectors to a common location; The ISR code performs the “ISR prologue” The C handler performs the following: while (there are still interrupts to process) { Get vector address from interrupt controller; Call interrupt handler; } The “ISR epilogue” is executed; (1) (2) (3) (4) (5) (6) Listing 9-3 Single interrupt vector for all interrupts 163 Chapter 9 L9-3(1) An interrupt occurs from any device. The interrupt controller activates the interrupt pin on the CPU. If there are other interrupts that occur after the first one, the interrupt controller will latch them and properly prioritize the interrupts. L9-3(2) The CPU vectors to a single interrupt handler address. In other words, all interrupts are to be handled by this one interrupt handler. L9-3(3) Execute the “ISR prologue” code needed by μC/OS-III. as previously described. This ensures that all ISRs will be able to make μC/OS-III “post” calls. L9-3(4) Call a μC/OS-III C handler, which will continue processing the ISR. This makes the code easier to write (and read). Notice that interrupts are not re-enabled. L9-3(5) The μC/OS-III C handler then interrogates the interrupt controller and asks it: “Who caused the interrupt?” The interrupt controller will either respond with a number (1 to N) or with the address of the interrupt handler for the interrupting device. Of course, the μC/OS-III C handler will know how to handle the specific interrupt controller since the C handler is written specifically for that controller. If the interrupt controller provides a number between 1 and N, the C handler simply uses this number as an index into a table (in ROM or RAM) containing the address of the interrupt service routine servicing the interrupting device. A RAM table is handy to change interrupt handlers at run-time. For many embedded systems, however, the table may also reside in ROM. If the interrupt controller responds with the address of the interrupt service routine, the C handler only needs to call this function. In both of the above cases, all interrupt handlers need to be declared as follows: void MyISRHandler (void); There is one such handler for each possible interrupt source (obviously, each having a unique name). The “while” loop terminates when there are no other interrupting devices to service. 164 Interrupt Management L9-3(6) The μC/OS-III “ISR epilogue” is executed to see if it is necessary to return to the interrupted task, or switch to a more important one. A couple of interesting points to notice: ■ If another device caused an interrupt before the C handler had a chance to query the interrupt controller, most likely the interrupt controller will capture that interrupt. In fact, if that second device happens to be a higher-priority interrupting device, it will most likely be serviced first, as the interrupt controller will prioritize the interrupts. ■ The loop will not terminate until all pending interrupts are serviced. This is similar to allowing nested interrupts, but better, since it is not necessary to redo the ISR prologue and epilogue. The disadvantage of this method is that a high priority interrupt that occurs after the servicing of another interrupt that has already started must wait for that interrupt to complete before it will be serviced. So, the latency of any interrupt, regardless of priority, can be as long as it takes to process the longest interrrupt. 9-5 EVERY INTERRUPT VECTORS TO A UNIQUE LOCATION If the interrupt controller vectors directly to the appropriate interrupt handler, each of the ISRs must be written in assembly language as described in section 9-2 “Typical μC/OS-III Interrupt Service Routine (ISR)” on page 159. This, of course, slightly complicates the design. However, copy and paste the majority of the code from one handler to the other and just change what is specific to the actual device. If the interrupt controller allows the user to query it for the source of the interrupt, it may be possible to simulate the mode in which all interrupts vector to the same location by simply setting all vectors to point to the same location. Most interrupt controllers that vector to a unique location, however, do not allow users to query it for the source of the interrupt since, by definition, having a unique vector for all interrupting devices should not be necessary. 165 Chapter 9 9-6 DIRECT AND DEFERRED POST METHODS μC/OS-III handles event posting from interrupts using two different methods: Direct and Deferred Post. The method used in the application is selected by changing the value of OS_CFG_ISR_POST_DEFERRED_EN in OS_CFG.H (this assumes you have access to μC/OS-III’s source code). When set to 0, μC/OS-III uses the Direct Post Method and when set to 1, μC/OS-III uses the Deferred Post Method. As far as application code and ISRs are concerned, these two methods are completely transparent. It is not necessary to change anything except the configuration value OS_CFG_ISR_POST_DEFERRED_EN to switch between the two methods. Of course, changing the configuration constant will require recompiling the product and μC/OS-III. Before explaining why to use one versus the other, let us review their differences. 9-6-1 DIRECT POST METHOD The Direct Post Method is used by μC/OS-II and is replicated in μC/OS-III. Figure 9-2 shows a task diagram of what takes place in a Direct Post. 1HZ +LJK 3ULRULW\ 7DVN  'HYLFH ,QWHUUXSW  ,65   ,QWHUUXSWHG 7DVN —&26,,, 'LVDEOHV,QWHUUXSWV LQ &ULWLFDO6HFWLRQV Figure 9-2 Direct Post Method 166 Interrupt Management F9-2(1) A device generates an interrupt. F9-2(2) The Interrupt Service Routine (ISR) responsible to handle the device executes (assuming interrupts are enabled). The device interrupt is generally the event a task is waiting for. The task waiting for this interrupt to occur either has a higher priority than the interrupted task, or lower (or equal) in priority. F9-2(3) If the ISR made a lower (or equal) priority task ready to run then upon completion of the ISR, μC/OS-III returns to the interrupted task exactly at the point the interrupt occurred. F9-2(4) If the ISR made a higher priority task ready to run, μC/OS-III will context switch to the new higher-priority task since the more important task was waiting for this device interrupt. F9-2(5) In the Direct Post Method, μC/OS-III must protect critical sections by disabling interrupts as some of these critical sections can be accessed by ISRs. The above discussion assumed that interrupts were enabled and that the ISR could respond quickly to the interrupting device. However, if the application code makes μC/OS-III service calls (and it will at some point), it is possible that interrupts would be disabled. When OS_CFG_ISR_POST_DEFERRED_EN is set to 0, μC/OS-III disables interrupts while accessing critical sections. Thus, interrupts will not be responded to until μC/OS-III re-enables interrupts. Of course, attempts were made to keep interrupt disable times as short as possible, but there are complex features of μC/OS-III that disable interrupts for a longer period than the user would like. The key factor in determining whether to use the Direct Post Method is generally the μC/OS-III interrupt disable time. This is fairly easy to determine since the μC/CPU files provided with the μC/OS-III port for the processor used includes code to measure maximum interrupt disable time. This code can be enabled (assumes you have the source code) for testing purposes and removed when ready to deploy the product. The user would typically not want to leave measurement code in production code to avoid introducing measurement artifacts. Once instrumented, let the application run for sufficiently long and read the variable CPU_IntDisMeasMaxRaw_cnts. The resolution (in time) of this variable depends on the timer used during the measurement. 167 Chapter 9 Determine the interrupt latency, interrupt response, interrupt recovery, and task latency by adding the execution times of the code involved for each, as shown below. Interrupt Latency = Maximum interrupt disable time; Interrupt Response = Interrupt latency + Vectoring to the interrupt handler + ISR prologue; Interrupt Recovery = Handling of the interrupting device + Posting a signal or a message to a task + OSIntExit() + OSIntCtxSw(); Task Latency = Interrupt response + Interrupt recovery + Time scheduler is locked; The execution times of the μC/OS-III ISR prologue, ISR epilogue, OSIntExit(), and OSIntCtxSw(), can be measured independently and should be fairly constant. It should also be easy to measure the execution time of a post call by using OS_TS_GET(). In the Direct Post Method, the scheduler is locked only when handling timers and therefore, task latency should be fast if there are not too many timers with short callbacks expiring at the same time. See Chapter 12, “Timer Management” on page 193. μC/OS-III is also able to measure the amount of time the scheduler is locked, providing task latency. 168 Interrupt Management 9-6-2 DEFERRED POST METHOD In the Deferred Post Method (OS_CFG_ISR_POST_DEFERRED_EN is set to 1), instead of disabling interrupts to access critical sections, μC/OS-III locks the scheduler. This avoids having other tasks access critical sections while allowing interrupts to be recognized and serviced. In the Deferred Post Method, interrupts‘ are almost never disabled. The Deferred Post Method is, however, a bit more complex as shown in Figure 9-3. 1HZ +LJK 3ULRULW\ 7DVN  ,QWHUUXSW 4XHXH 'HYLFH ,QWHUUXSW  ,65    ,QW4 7DVN  —&26,,, 'LVDEOHV ,QWHUUXSWV ,QWHUUXSWHG 7DVN —&26,,, /RFNV WKH 6FKHGXOHU Figure 9-3 Deferred Post Method block diagram F9-3(1) A device generates an interrupt. F9-3(2) The ISR responsible for handling the device executes (assuming interrupts are enabled). The device interrupt is the event that a task was waiting for. The task waiting for this interrupt to occur is either higher in priority than the interrupted task, lower, or equal in priority. F9-3(3) The ISR calls one of the post services to signal or send a message to a task. However, instead of performing the post operation, the ISR queues the actual post call along with arguments in a special queue called the Interrupt Queue. The ISR then makes the Interrupt Queue Handler Task ready to run. This task is internal to μC/OS-III and is always the highest priority task (i.e., Priority 0). 169 Chapter 9 F9-3(4) At the end of the ISR, μC/OS-III always context switches to the interrupt queue handler task, which then extracts the post command from the queue. We disable interrupts to prevent another interrupt from accessing the interrupt queue while the queue is being emptied. The task then re-enables interrupts, locks the scheduler, and performs the post call as if the post was performed at the task level all along. This effectively manipulates critical sections at the task level. F9-3(5) When the interrupt queue handler task empties the interrupt queue, it makes itself not ready to run and then calls the scheduler to determine which task must run next. If the original interrupted task is still the highest priority task, μC/OS-III will resume that task. F9-3(6) If, however, a more important task was made ready to run because of the post, μC/OS-III will context switch to that task. All the extra processing is performed to avoid disabling interrupts during critical sections of code. The extra processing time only consist of copying the post call and arguments into the queue, extracting it back out of the queue, and performing an extra context switch. Similar to the Direct Post Method, it is easy to determine interrupt latency, interrupt response, interrupt recovery, and task latency, by adding execution times of the pieces of code involved for each as shown below. Interrupt Latency 170 = Maximum interrupt disable time; Interrupt Response = Interrupt latency + Vectoring to the interrupt handler + ISR prologue; Interrupt Recovery Handling of the interrupting device + Posting a signal or a message to the Interrupt Queue + OSIntExit() + OSIntCtxSw() to Interrupt Queue Handler Task; = Interrupt Management Task Latency = Interrupt response + Interrupt recovery + Re-issue the post to the object or task + Context switch to task + Time scheduler is locked; The execution times of the μC/OS-III ISR prologue, ISR epilogue, OSIntExit(), and OSIntCtxSw(), can be measured independently and should be constant. It should also be easy to measure the execution time of a post call by using OS_TS_GET(). In fact, the post calls should be short in the Deferred Post Method because it only involves copying the post call and its arguments into the interrupt queue. The difference is that in the Deferred Post Method, interrupts are disabled for a very short amount of time and thus, the first three metrics should be fast. However, task latency is higher as μC/OS-III locks the scheduler to access critical sections. 171 Chapter 9 9-7 DIRECT VS. DEFERRED POST METHOD In the Direct Post Method, μC/OS-III disables interrupts to access critical sections. In comparison, while in the Deferred Post Method, μC/OS-III locks the scheduler to access the same critical sections. In the Deferred Post Method, μC/OS-III must still disable interrupts to access the interrupt queue. However, the interrupt disable time is very short and fairly constant. 7DVN 7DVN 7DVN 7DVN 7DVN ,QWHUUXSW 4XHXH 7DVN 7DVN 7DVN ,QW4 7DVN —&26,,, 'LVDEOHV ,QWHUUXSWV 7DVN 7DVN 7DVN 7DVN 7DVN 7DVN —&26,,, 'LVDEOHV ,QWHUUXSWV 'LUHFW3RVW0HWKRG —&26,,, /RFNV WKH 6FKHGXOHU 'HIHUUHG3RVW0HWKRG Figure 9-4 Direct vs. Deferred Post Methods If interrupt disable time is critical in the application because there are very fast interrupt sources and the interrupt disable time of μC/OS-III is not acceptable using the Direct Post Method, use the Deferred Post Method. However, if you are planning on using the features listed in Table 9-1, consider using the Deferred Post Method, described in the next section. 172 Interrupt Management Feature Reason Multiple tasks at the same priority Although this is an important feature of μC/OS-III, multiple tasks at the same priority create longer critical sections. However, if there are only a few tasks at the same priority, interrupt latency will be relatively small. If the user does not create multiple tasks at the same priority, the Direct Post Method is recommended. Event Flags Chapter 14, “Synchronization” on If multiple tasks are waiting on different events, going through all of the tasks waiting for events requires a fair amount of processing time, which page 251 means longer critical sections. If only a few tasks (approximately one to five) are waiting on an event flag group, the critical section will be short enough to use the Direct Post Method. Pend on multiple objects Pending on multiple objects is probably the most complex feature Chapter 16, “Pending On Multiple Objects” on page 313 provided by μC/OS-III and requires interrupts to be disabled for fairly long periods of time when using the Direct Post Method. If pending on multiple objects, the Deferred Post Method is highly recommended. If the application does not use this feature, the user may select the Direct Post Method. Broadcast on Post calls See OSSemPost() and OSQPost() μC/OS-III disables interrupts while processing a post to multiple tasks in a broadcast. descriptions. If not using the broadcast option, use the Direct Post Method. Note that broadcasts only apply to semaphores and message queues. Table 9-1 μC/OS-III features to avoid when using the Direct Post Method 9-8 THE CLOCK TICK (OR SYSTEM TICK) μC/OS-III-based systems generally require the presence of a periodic time source called the clock tick or system tick. A hardware timer configured to generate an interrupt at a rate between 10 and 1000 Hz provides the clock tick. A tick source may also be obtained by generating an interrupt from an AC power line (typically 50 or 60 Hz). In fact, one can easily derive 100 or 120 Hz by detecting zero crossings of the power line. 173 Chapter 9 The clock tick interrupt can be viewed as the system’s heartbeat. The rate is application specific and depends on the desired resolution of this time source. However, the faster the tick rate, the higher the overhead imposed on the system. The clock tick interrupt allows μC/OS-III to delay tasks for an integral number of clock ticks and provide timeouts when tasks are waiting for events to occur. The clock tick interrupt must call OSTimeTick(). The pseudocode for OSTimeTick() is shown in Listing 9-4. void OSTimeTick (void) { OSTimeTickHook(); #if OS_CFG_ISR_POST_DEFERRED_EN > 0u Get timestamp; Post “time tick” to the Interrupt Queue; #else Signal the Tick Task; Run the round-robin scheduling algorithm; Signal the timer task; #endif } (1) (2) (3) (4) (5) Listing 9-4 OSTimeTick() pseudocode L9-4(1) The time tick ISR starts by calling a hook function, OSTimeTickHook(). The hook function allows the implementer of the μC/OS-III port to perform additional processing when a tick interrupt occurs. In turn, the tick hook can call a user-defined tick hook if its corresponding pointer, OS_AppTimeTickHookPtr, is non-NULL. The reason the hook is called first is to give the application immediate access to this periodic time source. This can be useful to read sensors at a regular interval (not as subject to jitter), update Pulse Width Modulation (PWM) registers, and more. L9-4(2) If μC/OS-III is configured for the Deferred Post Method, μC/OS-III reads the current timestamp and defers the call to signal the tick task by placing an appropriate entry in the interrupt queue. The tick task will thus be signaled by the Interrupt Queue Handler Task. 174 Interrupt Management L9-4(3) If μC/OS-III is configured for the Direct Post Method, μC/OS-III signals the tick task so that it can process the time delays and timeouts. L9-4(4) μC/OS-III runs the round-robin scheduling algorithm to determine whether the time slot for the current task has expired. L9-4(5) The tick task is also used as the time base for the timers (see Chapter 13, “Resource Management” on page 209). A common misconception is that a system tick is always needed with μC/OS-III. In fact, many low-power applications may not implement the system tick because of the power required to maintain the tick list. In other words, it is not reasonable to continuously power down and power up the product just to maintain the system tick. Since μC/OS-III is a preemptive kernel, an event other than a tick interrupt can wake up a system placed in low power mode by either a keystroke from a keypad or other means. Not having a system tick means that the user is not allowed to use time delays and timeouts on system calls. This is a decision required to be made by the designer of the low-power product. 9-9 SUMMARY μC/OS-III provides services to manage interrupts. An ISR should be short in length, and signal or send a message to a task, which is responsible for servicing the interrupting device. ISRs that are short and do not need to signal or send a message to a task, are not required to do so. μC/OS-III supports processors that vector to a single ISR for all interrupting devices, or to a unique ISR for each device. μC/OS-III supports two methods: Direct and Deferred Post. The Direct Post Method assumes that μC/OS-III critical sections are protected by disabling interrupts. The Deferred Post Method locks the scheduler when μC/OS-III accesses critical sections of code. μC/OS-III assumes the presence of a periodic time source for applications requiring time delays and timeouts on certain services. 175 Chapter 9 176 Chapter 10 Pend Lists (or Wait Lists) A task is placed in a Pend List (also called a Wait List) when it is waiting on a semaphore to be signaled, a mutual exclusion semaphore to be released, an event flag group to be posted, or a message queue to be posted. See … For … Kernel Object Chapter 13, “Resource Management” on page 209 Semaphores Mutual Exclusion OS_SEM OS_MUTEX Semaphores Chapter 14, “Synchronization” on page 251 Semaphores Event Flags OS_SEM OS_FLAG_GRP Chapter 15, “Message Passing” on page 289 Message Queues OS_Q Table 10-1 Kernel objects that have Pend Lists A pend list is similar to the Ready List, except that instead of keeping track of tasks that are ready-to-run, the pend list keeps track of tasks waiting for an object to be posted. In addition, the pend list is sorted by priority; the highest priority task waiting on the object is placed at the head of the list, and the lowest priority task waiting on the object is placed at the end of the list. A pend list is a data structure of type OS_PEND_LIST, which consists of three fields as shown in Figure 10-1. 1EU(QWULHV 7DLO3WU +HDG3WU Figure 10-1 Pend List 177 Chapter 10 .NbrEntries Contains the current number of entries in the pend list. Each entry in the pend list points to a task that is waiting for the kernel object to be posted. .TailPtr Is a pointer to the last task in the list (i.e., the lowest priority task). .HeadPtr Is a pointer to the first task in the list (i.e., the highest priority task). Figure 10-2 indicates that each kernel object using a pend list contains the same three fields at the beginning of the kernel object that we called an OS_PEND_OBJ. Notice that the first field is always a “Type” which allows μC/OS-III to know if the kernel object is a semaphore, a mutual exclusion semaphore, an event flag group, or a message queue object. 26B6(0 26B3(1'B2%- 26B087(; 26B)/$*B*53 26B4 7\SH 7\SH 7\SH 7\SH 1DPH3WU 1DPH3WU 1DPH3WU 1DPH3WU 7DLO3WU  +HDG3WU  7DLO3WU +HDG3WU 7DLO3WU  +HDG3WU  7DLO3WU +HDG3WU &WU 2ZQHU7&%3WU )ODJV 0VJ4 76 2ZQHU2ULJLQDO3ULR 76 76 2ZQHU1HVWLQJ&WU 76 Figure 10-2 OS_PEND_OBJ at the beginning of certain kernel objects Table 10-2 shows that the “Type” field of each of the above objects is initialized to contain four ASCII characters when the respective object is created. This allows the user to identify these objects when performing a memory dump using a debugger. Kernel Object Type Semaphore ‘S” “E” “M” “A” Mutual Exclusion Semaphore ‘M” “U” “T” “X” Event Flag Group ‘F” “L” “A” “G” Message Queue ‘Q” “U” “E” “U” Table 10-2 Kernel objects with initialized “Type” field 178 Pend Lists (or Wait Lists) A pend list does not actually point to a task’s OS_TCB, but instead points to OS_PEND_DATA objects as shown in Figure 10-3. Also, an OS_PEND_DATA structure is allocated dynamically on the current task’s stack when a task is placed on a pend list. This implies that a task stack needs to be able to allocate storage for this data structure. 26B3(1'B'$7$ 3UHY3WU 1H[W3WU 7&%3WU 3HQG2EM3WU 5G\2EM3WU 5G\0VJ3WU 5G\0VJ6L]H 5G\76 Figure 10-3 Pend Data .PrevPtr Is a pointer to an OS_PEND_DATA entry in the pend list. This pointer points to a higher or equal priority task waiting on the kernel object. .NextPtr Is a pointer to an OS_PEND_DATA entry in the pend list. This pointer points to a lower or equal priority task waiting on the kernel object. .TCBPtr Is a pointer to the OS_TCB of the task waiting on the pend list. .PendObjPtr Is a pointer to the kernel object that the task is pending on. In other words, this pointer can point to an OS_SEM, OS_MUTEX, OS_FLAG_GRP or OS_Q by using an OS_PEND_OBJ as the common data structure. .RdyObjPtr Is a pointer to the kernel object that is ready if the task actually waits for multiple kernel objects. See Chapter 16, “Pending On Multiple Objects” on page 313 for more on this. 179 Chapter 10 .RdyMsgPtr Is a pointer to the message posted through OSQPost() if the task is pending on multiple kernel objects. Again, see Chapter 16, “Pending On Multiple Objects” on page 313. .RdyTS Is a timestamp of when the kernel object was posted. This is used when a task pends on multiple kernel objects as described in Chapter 16, “Pending On Multiple Objects” on page 313. Figure 10-4 exhibits how all data structures connect to each other when tasks are inserted in a pend list. This drawing assumes that there are two tasks waiting on a semaphore. (1) OS_SEM (4) Type NamePtr OS_PEND_OBJ TailPtr #=2 HeadPtr Ctr OS_PEND_LIST TS (2) (7) 0 (5) (3) PrevPtr (7) NextPtr NextPtr TCBPtr TCBPtr PendObjPtr PendObjPtr RdyObjPtr RdyObjPtr RdyMsgPtr RdyMsgPtr RdyMsgSize RdyMsgSize RdyTS (5) OS_PEND_DATA (6) OS_TCB Higher Priority Task (7) PrevPtr 0 RdyTS OS_PEND_DATA (6) OS_TCB Lower Priority Task Figure 10-4 Pend Data 180 Pend Lists (or Wait Lists) F10-4(1) The OS_SEM data type contains an OS_PEND_OBJ, which in turn contains an OS_PEND_LIST. The .NbrEntries field in the pend list indicates that there are two tasks waiting on the semaphore. F10-4(2) The .HeadPtr field of the pend list points to the OS_PEND_DATA structure associated with the highest priority task waiting on the semaphore. F10-4(3) The .TailPtr field of the pend list points to the OS_PEND_DATA structure associated with the lowest priority task waiting on the semaphore. F10-4(4) Both OS_PEND_DATA structures in turn point back to the OS_SEM data structure. The pointers think they are pointing to an OS_PEND_OBJ. We know that the OS_PEND_OBJ is a semaphore by examining the .Type field of the OS_PEND_OBJ. F10-4(5) Each OS_PEND_DATA structure points to its respective OS_TCB. In other words, we know which task is pending on the semaphore. F10-4(6) Each task points back to the OS_PEND_DATA structure. F10-4(7) Finally, the OS_PEND_DATA structure forms a doubly linked list so that the μC/OS-III can easily add or remove entries in this list. Although this may seem complex, the reasoning will become apparent in Chapter 16, “Pending On Multiple Objects” on page 313. For now, assume all of the links are necessary. Table 10-3 shows the functions that μC/OS-III uses to manipulate entries in a pend list. These functions are internal to μC/OS-III and the application code must never call them. The code is found in OS_CORE.C. 181 Chapter 10 Function Description OS_PendListChangePrio() Change the priority of a task in a pend list OS_PendListInit() Initialize a pend list OS_PendListInsertHead() Insert an OS_PEND_DATA at the head of the pend list OS_PendListInsertPrio() Insert an OS_PEND_DATA in priority order in the pend list OS_PendListRemove() Remove multiple OS_PEND_DATA from the pend list OS_PendListRemove1() Remove single OS_PEND_DATA from the pend list Table 10-3 Pend List access functions 10-1 SUMMARY μC/OS-III keeps track of tasks waiting for semaphores, mutual exclusion semaphores, event flag groups and message queues using pend lists. A pend list consists of a data structure of type OS_PEND_LIST. The pend list is further encapsulated into another data type called an OS_PEND_OBJ. Tasks are not directly linked to the pend list but instead are linked through an intermediate data structure called an OS_PEND_DATA which is allocated on the stack of the task waiting on the kernel object. Application code must not access pend lists, since these are internal to μC/OS-III. 182 Chapter 11 Time Management μC/OS-III provides time-related services to the application programmer. In Chapter 9, “Interrupt Management” on page 157, it was established that μC/OS-III generally requires (as do most kernels) that the user provide a periodic interrupt to keep track of time delays and timeouts. This periodic time source is called a clock tick and should occur between 10 and 1000 times per second, or Hertz (see OS_CFG_TICK_RATE_HZ in OS_CFG_APP.H). The actual frequency of the clock tick depends on the desired tick resolution of the application. However, the higher the frequency of the ticker, the higher the overhead. μC/OS-III provides a number of services to manage time as summarized in Table 11-1, and the code is found in OS_TIME.C. Function Name Operation OSTimeDly() Delay execution of a task for “n” ticks OSTimeDlyHMSM() Delay a task for a user specified time in HH:MM:SS.mmm OSTimeDlyResume() Resume a delayed task OSTimeGet() Obtain the current value of the tick counter OSTimeSet() Set the tick counter to a new value OSTimeTick() Signal the occurrence of a clock tick Table 11-1 Time Services API summary The application programmer should refer to Appendix A, “μC/OS-III API Reference Manual” on page 375 for a detailed description of these services. 183 Chapter 11 11-1 OSTimeDly() A task calls this function to suspend execution until some time expires. The calling function will not execute until the specified time expires. This function allows three modes: relative, periodic and absolute. Listing 11-1 shows how to use OSTimeDly() in relative mode. void MyTask (void *p_arg) { OS_ERR err; : : while (DEF_ON) { : : OSTimeDly(2, OS_OPT_TIME_DLY, &err); /* Check “err” */ : : } } (1) (2) (3) (4) Listing 11-1 OSTimeDly() - Relative L11-1(1) The first argument specifies the amount of time delay (in number of ticks) from when the function is called. For example if the tick rate (OS_CFG_TICK_RATE_HZ in OS_CFG_APP.H) is set to 1000 Hz, the user is asking to suspend the current task for approximately 2 milliseconds. However, the value is not accurate since the count starts from the next tick which could occur almost immediately. This will be explained shortly. L11-1(2) Specifying OS_OPT_TIME_DLY indicates that the user wants to use “relative” mode. L11-1(3) As with most μC/OS-III services an error return value will be returned. The example should return OS_ERR_NONE as the arguments are all valid. Refer to Chapter 11, “Time Management” on page 183 for a list of possible error codes. 184 Time Management L11-1(4) Always check the error code returned by μC/OS-III. If “err” does not contain OS_ERR_NONE, OSTimeDly() did not perform the intended work. For example, another task could remove the time delay suspension by calling OSTimeDlyResume() and when MyTask() returns, it would not have returned because the time had expired. As mentioned above, the delay is not accurate. Refer to Figure 11-1 and its description below to understand why. 3ULRULW\    7LFN $OO +37V /37      7LPH  WLFNV WLFNV Figure 11-1 OSTimeDly() - Relative F11-1(1) We get a tick interrupt and μC/OS-III services the ISR. F11-1(2) At the end of the ISR, all Higher Priority Tasks (HPTs) execute. The execution time of HPTs is unknown and can vary. F11-1(3) Once all HPTs have executed, μC/OS-III runs the task that has called OSTimeDly() as shown above. For the sake of discussion, it is assumed that this task is a lower priority task (LPT). 185 Chapter 11 F11-1(4) The task calls OSTimeDly() and specifies to delay for two ticks in “relative” mode. At this point, μC/OS-III places the current task in the tick list where it will wait for two ticks to expire. The delayed task consumes zero CPU time while waiting for the time to expire. F11-1(5) The next tick occurs. If there are HPTs waiting for this particular tick, μC/OS-III will schedule them to run at the end of the ISR. F11-1(6) The HPTs execute. F11-1(7) The next tick interrupt occurs. This is the tick that the LPT was waiting for and will now be made ready to run by μC/OS-III. F11-1(8) Since there are no HPTs to execute on this tick, μC/OS-III switches to the LPT. F11-1(9) Given the execution time of the HPTs, the time delay is not exactly two ticks, as requested. In fact, it is virtually impossible to obtain a delay of exactly the desired number of ticks. One might ask for a delay of two ticks, but the very next tick could occur almost immediately after calling OSTimeDly()! Just imagine what might happen if all HPTs took longer to execute and pushed (3) and (4) further to the right. In this case, the delay would actually appear as one tick instead of two. OSTimeDly() can also be called with the OS_OPT_TIME_PERIODIC option as shown in Listing 11-2. This option allows delaying the task until the tick counter reaches a certain periodic match value and thus ensures that the spacing in time is always the same as it is not subject to CPU load variations. μC/OS-III determines the “match value” of OSTickCtr to determine when the task will need to wake up based on the desired period. This is shown in Figure 11-2. μC/OS-III checks to ensure that if the match is computed such that it represents a value that has already gone by then, the delay will be zero. 186 Time Management Tick Task Time 4 ticks Figure 11-2 OSTimeDly() - Periodic void MyTask (void *p_arg) { OS_ERR err; : : while (DEF_ON) { OSTimeDly(4, OS_OPT_TIME_PERIODIC, (1) (2) &err); /* Check “err” */ (3) : : } } Listing 11-2 OSTimeDly() - Periodic L11-2(1) The first argument specifies the period for the task to execute, specifically every four ticks. Of course, if the task is a low-priority task, μC/OS-III only schedules and runs the task based on its priority relative to what else needs to be executed. L11-2(2) Specifying OS_OPT_TIME_PERIODIC indicates that the task is to be ready to run when the tick counter reaches the desired period from the previous call. L11-2(3) You should always check the error code returned by μC/OS-III. 187 Chapter 11 Relative and Periodic modes might not look different, but they are. In Relative mode, it is possible to miss one of the ticks when the system is heavily loaded, missing a tick or more on occasion. In Periodic mode, the task may still execute later, but it will always be synchronized to the desired number of ticks. In fact, Periodic mode is the preferred mode to use to implement a time-of-day clock. Finally, you can use the absolute mode to perform a specific action at a fixed time after power up. For example, turn off a light 10 seconds after the product powers up. In this case, you would specify OS_OPT_TIME_MATCH while “dly” actually corresponds to the desired value of OSTickCtr you want to reach. To summarize, the task will wake up when OSTickCtr reaches the following value: Value of “opt” Task wakes up when OS_OPT_TIME_DLY OSTickCtr + dly OS_OPT_TIME_PERIODIC OSTCBCurPtr->TickCtrPrev + dly OS_OPT_TIME_MATCH dly 188 Time Management 11-2 OSTimeDlyHMSM() A task may call this function to suspend execution until some time expires by specifying the length of time in a more user-friendly way. Specifically, specify the delay in hours, minutes, seconds, and milliseconds (thus the HMSM). This function only works in “Relative” mode. Listing 11-3 indicates how to use OSTimeDlyHMSM(). void MyTask (void *p_arg) { OS_ERR err; : : while (DEF_ON) { : : OSTimeDlyHMSM(0, 0, 1, 0, OS_OPT_TIME_HMSM_STRICT, &err); /* Check “err” */ : : } } (1) (2) (3) Listing 11-3 OSTimeDlyHMSM() L11-3(1) The first four arguments specify the amount of time delay (in hours, minutes, seconds, and milliseconds) from this point in time. In the above example, the task should delay by 1 second. The resolution greatly depends on the tick rate. For example, if the tick rate (OS_CFG_TICK_RATE_HZ in OS_CFG_APP.H) is set to 1000 Hz there is technically a resolution of 1 millisecond. If the tick rate is 100 Hz then the delay of the current task is in increments of 10 milliseconds. Again, given the relative nature of this call, the actual delay may not be accurate. L11-3(2) Specifying OS_OPT_TIME_HMSM_STRICT verifies that the user strictly passes valid values for hours, minutes, seconds and milliseconds. Valid hours are 0 to 99, valid minutes are 0 to 59, valid seconds are 0 to 59, and valid milliseconds are 0 to 999. 189 Chapter 11 If specifying OS_OPT_TIME_HMSM_NON_STRICT, the function will accept nearly any value for hours (between 0 to 999), minutes (from 0 to 9999), seconds (any value, up to 65,535), and milliseconds (any value, up to 4,294,967,295). OSTimeDlyHMSM(203, 101, 69, 10000) may be accepted. Whether or not this makes sense is a different story. The reason hours is limited to 999 is that time delays typically use 32-bit values to keep track of ticks. If the tick rate is set at 1000 Hz then, it is possible to only track 4,294,967 seconds, which corresponds to 1,193 hours, and therefore 999 is a reasonable limit. L11-3(3) As with most μC/OS-III services the user will receive an error return value. The example should return OS_ERR_NONE since the arguments are all valid. Refer to Appendix A, “μC/OS-III API Reference Manual” on page 375 for a list of possible error codes. Even though μC/OS-III allows for very long delays for tasks, it is actually not recommended to delay tasks for a long time. There is no indication that the task is actually “alive” unless it is possible to monitor the amount of time remaining for the delay. It is better to have the task wake up approximately every minute or so, and have it “tell you” that it is still ok. OSTimeDly() and OSTimeDlyHMSM() are often used to create periodic tasks (tasks that execute periodically). For example, it is possible to have a task that scans a keyboard every 50 milliseconds and another task that reads analog inputs every 10 milliseconds, etc. 190 Time Management 11-3 OSTimeDlyResume() A task can resume another task that called OSTimeDly() or OSTimeDlyHMSM() by calling OSTimeDlyResume(). Listing 11-4 shows how to use OSTimeDlyResume(). The task that delayed itself will not know that it was resumed, but will think that the delay expired. Because of this, use this function with great care. OS_TCB void MyTaskTCB; MyTask (void *p_arg) { OS_ERR err; : : while (1) { : : OSTimeDly(10, OS_OPT_TIME_DLY, &err); /* Check “err” */ : : } } void MyOtherTask (void *p_arg) { OS_ERR err; : : while (1) { : : OSTimeDlyResume(&MyTaskTCB, &err); /* Check “err” */ : : } } Listing 11-4 OSTimeDlyResume() 191 Chapter 11 11-4 OSTimeSet() AND OSTimeGet() μC/OS-III increments a tick counter every time a tick interrupt occurs. This counter allows the application to make coarse time measurements and have some notion of time (after power up). OSTimeGet() allows the user to take a snapshot of the tick counter. As shown in a previous section, use this value to delay a task for a specific number of ticks and repeat this periodically without losing track of time. OSTimeSet() allows the user to change the current value of the tick counter. Although μC/OS-III allows for this, it is recommended to use this function with great care. 11-5 OSTimeTick() The tick Interrupt Service Routine (ISR) must call this function every time a tick interrupt occurs. μC/OS-III uses this function to update time delays and timeouts on other system calls. OSTimeTick() is considered an internal function to μC/OS-III. 11-6 SUMMARY μC/OS-III provides services to applications so that tasks can suspend their execution for user-defined time delays. Delays are either specified by a number of clock ticks or hours, minutes, seconds, and milliseconds. Application code can resume a delayed task by calling OSTimeDlyResume(). However, its use is not recommended because resumed task will not know that they were resumed as opposed to the time delay expired. μC/OS-III keeps track of the number of ticks occurring since power up or since the number of ticks counter was last changed by OSTimeSet(). The counter may be read by the application code using OSTimeGet(). 192 Chapter 12 Timer Management μC/OS-III provides timer services to the application programmer and code to handle timers is found in OS_TMR.C. Timer services are enabled when setting OS_CFG_TMR_EN to 1 in OS_CFG.H. Timers are down counters that perform an action when the counter reaches zero. The user provides the action through a callback function (or simply callback). A callback is a user-declared function that will be called when the timer expires. The callback can be used to turn a light on or off, start a motor, or perform other actions. However, it is important to never make blocking calls within a callback function (i.e., call OSTimeDly(), OSTimeDlyHMSM(), OS???Pend(), or anything that causes the timer task to block or be deleted). Timers are useful in protocol stacks (retransmission timers, for example), and can also be used to poll I/O devices at predefined intervals. An application can have any number of timers (limited only by the amount of RAM available). Timer services in μC/OS-III start with the OSTmr???() prefix, and the services available to the application programmer are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. The resolution of all the timers managed by μC/OS-III is determined by the configuration constant: OS_CFG_TMR_TASK_RATE_HZ, which is expressed in Hertz (Hz). So, if the timer task (described later) rate is set to 10, all timers have a resolution of 1/10th of a second (ticks in the diagrams to follow). In fact, this is the typical recommended value for the timer task. Timers are to be used with “coarse” granularity. 193 Chapter 12 μC/OS-III provides a number of services to manage timers as summarized in Table 12-1. Function Name Operation OSTmrCreate() Create and specify the operating mode of the timer. OSTmrDel() Delete a timer. OSTmrRemainGet() Obtain the remaining time left before the timer expires. OSTmrStart() Start (or restart) a timer. OSTmrStateGet() Obtain the current state of a timer. OSTmrStop() Stop the countdown process of a timer. Table 12-1 Timer API summary A timer needs to be created before it can be used. Create a timer by calling OSTmrCreate() and specify a number of arguments to this function based on how the timer is to operate. Once the timer operation is specified, its operating mode cannot be changed unless the timer is deleted and recreated. The function prototype for OSTmrCreate() is shown below as a quick reference: void OSTmrCreate (OS_TMR CPU_CHAR OS_TICK OS_TICK OS_OPT OS_TMR_CALLBACK_PTR void OS_ERR *p_tmr, *p_name, dly, period, opt, p_callback, *p_callback_arg, *p_err) /* /* /* /* /* /* /* Pointer to timer Name of timer, ASCII Initial delay Repeat period Options Fnct to call at 0 Arg. to callback */ */ */ */ */ */ */ Once created, a timer can be started (or restarted) and stopped as often as is necessary. Timers can be created to operate in one of three modes: One-shot, Periodic (no initial delay), and Periodic (with initial delay). 194 Timer Management 12-1 ONE-SHOT TIMERS As its name implies, a one-shot timer will countdown from its initial value, call the callback function when it reaches zero, and stop. Figure 12-1 shows a timing diagram of this operation. The countdown is initiated by calling OSTmrStart(). At the completion of the time delay, the callback function is called, assuming a callback function was provided when the timer was created. Once completed, the timer does not do anything unless restarted by calling OSTmrStart(), at which point the process starts over. Terminate the countdown process of a timer (before it reaches zero) by calling OSTmrStop(). In this case, specify that the callback function be called or not. 267PU&UHDWH 7LFNV 267PU6WDUW GO\ WLFNV 7LPH &DOOEDFN &DOOHG Figure 12-1 One Shot Timers (dly > 0, period == 0) As shown in Figure 12-2, a one-shot timer is retriggered by calling OSTmrStart() before the timer reaches zero. This feature can be used to implement watchdogs and similar safeguards. 267PU6WDUW 267PU&UHDWH 7LFNV GO\ WLFNV 7LPH &DOOEDFN &DOOHG Figure 12-2 Retriggering a One Shot Timer 195 Chapter 12 12-2 PERIODIC (NO INITIAL DELAY) As indicated in Figure 12-3, timers can be configured for periodic mode. When the countdown expires, the callback function is called, the timer is automatically reloaded, and the process is repeated. If specifying a delay of zero (i.e., dly == 0) when the timer is created, when started, the timer immediately uses the “period” as the reload value. Calling OSTmrStart() at any point in the countdown restarts the process. 267PU&UHDWH 7LFNV 267PU6WDUW SHULRG WLFNV 7LPH &DOOEDFN &DOOHG &DOOEDFN &DOOHG &DOOEDFN &DOOHG Figure 12-3 Periodic Timers (dly == 0, period > 0) 12-3 PERIODIC (WITH INITIAL DELAY) As shown in Figure 12-4, timers can be configured for periodic mode with an initial delay that is different than its period. The first countdown count comes from the “dly” argument passed in the OSTmrCreate() call, and the reload value is the “period”. Calling OSTmrStart() restarts the process including the initial delay. 267PU&UHDWH 267PU6WDUW $XWRUHORDG 7LFNV GO\ WLFNV SHULRG WLFNV 7LPH &DOOEDFN &DOOHG &DOOEDFN &DOOHG &DOOEDFN &DOOHG &DOOEDFN &DOOHG &DOOEDFN &DOOHG Figure 12-4 Periodic Timers (dly > 0, period > 0) 196 Timer Management 12-4 TIMER MANAGEMENT INTERNALS 12-4-1 TIMER MANAGEMENT INTERNALS - TIMERS STATES Figure 12-5 shows the state diagram of a timer. Tasks can call OSTmrStateGet() to find out the state of a timer. Also, at any time during the countdown process, the application code can call OSTmrRemainGet() to find out how much time remains before the timer reaches zero (0). The value returned is expressed in “timer ticks.” If timers are decremented at a rate of 10 Hz then a count of 50 corresponds to 5 seconds. If the timer is in the stop state, the time remaining will correspond to either the initial delay (one shot or periodic with initial delay), or the period if the timer is configured for periodic without initial delay. 6WRSSHG  267PU&UHDWH 267PU'HO 8QXVHG  267PU6WRS &DOOEDFN 267PU6WDUW 2QH6KRW GO\ ([SLUHG &DOOEDFN 267PU'HO &RPSOHWHG  5XQQLQJ  267PU6WDUW 267PU6WDUW RU 3HULRGLF &DOOEDFN 267PU'HO Figure 12-5 Timer State Diagram 197 Chapter 12 F12-5(1) The “Unused” state is a timer that has not been created or has been “deleted.” In other words, μC/OS-III does not know about this timer. F12-5(2) When creating a timer or calling OSTmrStop(), the timer is placed in the “stopped” state. F12-5(3) A timer is placed in running state when calling OSTmrStart(). The timer stays in that state unless it’s stopped, deleted, or completes its one shot. F12-5(4) The “Completed” state is the state a one-shot timer is in when its delay expires. 12-4-2 TIMER MANAGEMENT INTERNALS - OS_TMR A timer is a kernel object as defined by the OS_TMR data type (see OS.H) as shown in Listing 12-1. The services provided by μC/OS-III to manage timers are implemented in the file OS_TMR.C. A μC/OS-III licensee has access to the source code. In this case, timer services are enabled at compile time by setting the configuration constant OS_CFG_TMR_EN to 1 in OS_CFG.H. typedef struct os_tmr struct os_tmr { OS_OBJ_TYPE CPU_CHAR OS_TMR_CALLBACK_PTR void OS_TMR OS_TMR OS_TICK OS_TICK OS_TICK OS_TICK OS_OPT OS_STATE }; OS_TMR; Type; *NamePtr; CallbackPtr; *CallbackPtrArg; *NextPtr; *PrevPtr; Match; Remain; Dly; Period; Opt; State; (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) Listing 12-1 OS_TMR data type 198 Timer Management L12-1(1) In μC/OS-III, all structures are given a data type. In fact, all data types start with “OS_” and are all uppercase. When a timer is declared, simply use OS_TMR as the data type of the variable used to declare the timer. L12-1(2) The structure starts with a “Type” field, which allows it to be recognized by μC/OS-III as a timer. Other kernel objects will also have a “Type” as the first member of the structure. If a function is passed a kernel object, μC/OS-III is able to confirm that it is passed the proper data type. For example, if passing a message queue (OS_Q) to a timer service (for example OSTmrStart()) then μC/OS-III will be able to recognize that an invalid object was passed, and return an error code accordingly. L12-1(3) Each kernel object can be given a name for easier recognition by debuggers or μC/Probe. This member is simply a pointer to an ASCII string which is assumed to be NUL terminated. L12-1(4) The .CallbackPtr member is a pointer to a function that is called when the timer expires. If a timer is created and passed a NULL pointer, a callback would not be called when the timer expires. L12-1(5) If there is a non-NULL .CallbackPtr then the application code could have also specified that the callback be called with an argument when the timer expires. This is the argument that would be passed in this call. L12-1(6) .NextPtr and .PrevPtr are pointers used to link a timer in a doubly linked list. These are described later. L12-1(7) A timer expires when the timer manager variable OSTmrTickCtr reaches the value stored in a timer’s .Match field. This is also described later. L12-1(8) The .Remain contains the amount of time remaining for the timer to expire. This value is updated once per OS_CFG_TMR_WHEEL_SIZE (see OS_CFG_APP.H) that the timer task executes (described later). The value is expressed in multiples of 1/OS_CFG_TMR_TASK_RATE_HZ of a second (see OS_CFG_APP.H). 199 Chapter 12 L12-1(9) The .Dly field contains the one-shot time when the timer is configured (i.e., created) as a one-shot timer and the initial delay when the timer is created as a periodic timer. The value is expressed in multiples of 1/OS_CFG_TMR_TASK_RATE_HZ of a second (see OS_CFG_APP.H). L12-1(10) The .Period is the timer period when the timer is created to operate in periodic mode. The value is expressed in multiples of 1/OS_CFG_TMR_TASK_RATE_HZ of a second (see OS_CFG_APP.H). L12-1(11) The .Opt field contains options as passed to OSTmrCreate(). L12-1(12) The .State field represents the current state of the timer (see Figure 12-5). Even if the internals of the OS_TMR data type are understood, the application code should never access any of the fields in this data structure directly. Instead, always use the Application Programming Interfaces (APIs) provided with μC/OS-III. 12-4-3 TIMER MANAGEMENT INTERNALS - TIMER TASK OS_TmrTask() is a task created by μC/OS-III (assumes setting OS_CFG_TMR_EN to 1 in OS_CFG.H) and its priority is configurable by the user through μC/OS-III’s configuration file OS_CFG_APP.H (see OS_CFG_TMR_TASK_PRIO). OS_TmrTask() is typically set to a medium priority. OS_TmrTask() is a periodic task and uses the same interrupt source used to generate clock ticks. However, timers are generally updated at a slower rate (i.e., typically 10 Hz or so) and thus, the timer tick rate is divided down in software. If the tick rate is 1000 Hz and the desired timer rate is 10 Hz then the timer task will be signaled every 100th tick interrupt as shown in Figure 12-6. 200 Timer Management 7LPHU WR+] 7LFN ,65 6LJQDOHG DW 7LFN5DWH7LPHU5DWH 7LPHU 7DVN 1 /LVWRIWLPHUVWRXSGDWH Figure 12-6 Tick ISR and Timer Task relationship Figure 12-7 shows timing diagram associated with the timer management task. Priority Tick ISR Higher All Higher Priority Tasks (1) (2) HPT (3) Lower TimerTask (4) (5) (6) (7) Time Figure 12-7 Timing Diagram F12-7(1) The tick ISR occurs and assumes interrupts are enabled and executes. F12-7(2) The tick ISR signals the tick task that it is time for it to update timers. 201 Chapter 12 F12-7(3) The tick ISR terminates, however there are higher priority tasks that need to execute (assuming the timer task has a lower priority). Therefore, μC/OS-III runs the higher priority task(s). F12-7(4) When all higher priority tasks have executed, μC/OS-III switches to the timer task and determines that there are three timers that expired. F12-7(5) The callback for the first timer is executed. F12-7(6) The callback for the second expired timer is executed. F12-7(7) The callback for the third expired timer is executed. There are a few interesting things to notice: ■ Execution of the callback functions is performed within the context of the timer task. This means that the application code will need to make sure there is sufficient stack space for the timer task to handle these callbacks. ■ The callback functions are executed one after the other based on the order they are found in the timer list. ■ The execution time of the timer task greatly depends on how many timers expire and how long each of the callback functions takes to execute. Since the callbacks are provided by the application code they have a large influence on the execution time of the timer task. ■ The timer callback functions must never wait on events that would delay the timer task for excessive amounts of time, if not forever. ■ Callbacks are called with the scheduler locked, so you should ensure that callbacks execute as quickly as possible. 202 Timer Management 12-4-4 TIMER MANAGEMENT INTERNALS - TIMER LIST μC/OS-III might need to literally maintain hundreds of timers (if an application requires that many). The timer list management needs to be implemented such that it does not take too much CPU time to update the timers. The timer list works similarly to a tick list as shown in Figure 12-8. 26&IJB7PU:KHHO>@ >@ >@ >@ >@ >@                  267PU7LFN&WU >26B&)*B705B:+((/B6,=(@ >26B&)*B705B:+((/B6,=(@      1EU(QWULHV0D[   )LUVW3WU 1EU(QWULHV  Figure 12-8 Empty Timer List F12-8(1) The timer list consists of a table (OSCfg_TmrWheel[]) and a counter (OSTmrTickCtr). F12-8(2) The table contains up to OS_CFG_TMR_WHEEL_SIZE entries, which is a compile time configuration value (see OS_CFG_APP.H). The number of entries depends on the amount of RAM available to the processor and the maximum number of timers in the application. A good starting point for OS_CFG_TMR_WHEEL_SIZE might be: #Timers/4. It is not recommended to make OS_CFG_TMR_WHEEL_SIZE an even multiple of the timer task rate. In other words, if the timer task is 10 Hz, avoid setting OS_CFG_TMR_WHEEL_SIZE to 10 or 100 (use 11 or 101 instead). Also, use prime numbers for the timer wheel size. Although it is not really possible to plan at compile time what will happen at run time, ideally the number of timers waiting in each entry of the table is distributed uniformly. 203 Chapter 12 Each entry in the table contains three fields: .NbrEntriesMax, .NbrEntries and .FirstPtr. .NbrEntries indicates how many timers are linked to this table entry. .NbrEntriesMax keeps track of the highest number of entries in the table. Finally, .FirstPtr contains a pointer to a doubly linked list of timers (through the tasks OS_TMR) belonging into the list at that table position. F12-8(3) The counter is incremented by OS_TmrTask() every time the tick ISR signals the task. Timers are inserted in the timer list by calling OSTmrStart(). However, a timer must be created before it can be used. An example to illustrate the process of inserting a timer in the timer list is as follows. Let’s assume that the timer list is completely empty, OS_CFG_TMR_WHEEL_SIZE is configured to 9, and the current value of OSTmrTickCtr is 12 as shown in Figure 12-9. A timer is placed in the timer list when calling OSTmrStart(), and assumes that the timer was created with a delay of 1 and that this timer will be a one-shot timer as follows: OS_TMR OS_TMR MyTmr1; MyTmr2; void MyTask (void *p_arg) { OS_ERR err; while (DEF_ON) { : OSTmrCreate((OS_TMR *)&MyTmr1, (OS_CHAR *)“My Timer #1”, (OS_TICK )1, (OS_TICK )0, (OS_OPT )OS_OPT_TMR_ONE_SHOT, (OS_TMR_CALLBACK_PTR)0; (OS_ERR *)&err; /* Check ’err” */ OSTmrStart ((OS_TMR *)&MyTmr1, (OS_ERR *)&err); /* Check “err” */ // Continues in the next code listing! Listing 12-2 Creating and Starting a timer 204 Timer Management Since OSTmrTickCtr has a value of 12, the timer will expire when OSTmrTickCtr reaches 13, or during the next time the timer task is signaled. Timers are inserted in the OSCfg_TmrWheel[] table using the following equation: MatchValue = OSTmrTickCtr + dly Index into OSCfg_TmrWheel[] = MatchValue % OS_CFG_TMR_WHEEL_SIZE Where “dly” (in this example) is the value passed in the third argument of OSTmrCreate() (i.e., 1 in this example). Again, using the example, we arrive at the following: MatchValue = 12 + 1 Index into OSCfg_TickWheel[] = 13 % 9 or, MatchValue = 13 Index into OSCfg_TickWheel[] = 4 Because of the “circular” nature of the table (a modulo operation using the size of the table), the table is referred to as a timer wheel, and each entry is a spoke in the wheel. The timer is entered at index 4 in the timer wheel, OSCfg_TmrWheel[]. In this case, the OS_TMR is placed at the head of the list (i.e., pointed to by OSCfg_TmrWheel[4].FirstPtr), and the number of entries at index 4 is incremented (i.e., OSCfg_TmrWheel[4].NbrEntries will be 1). “MatchValue” is placed in the OS_TMR field .Match. Since this is the first timer inserted in the timer list at index 4, the .NextPtr and .PrevPtr both point to NULL. OSCfg_TmrWheel[] [0] 0 [1] 0 0 0 [2] [3] [4] [5] [6] [7] [8] 0 0 0 0 0 0 0 1 0 1 0 0 0 .NextPtr 0 0 0 0 0 0 .NbrEntriesMax 0 0 .PrevPtr 0 .Remain = 1 0 .Match = 13 0 OS_TMR OSTmrTickCtr == 12 .FirstPtr .NbrEntries Figure 12-9 Inserting a timer in the timer list 205 Chapter 12 The code below shows creating and starting another timer. This is performed “before” the timer task is signaled. // Continuation of code from previous code listing. : : OSTmrCreate((OS_TMR *)&MyTmr2, (OS_CHAR *)“My Timer #2”, (OS_TICK )10, (OS_TICK )0, (OS_OPT )OS_OPT_TMR_ONE_SHOT, (OS_TMR_CALLBACK_PTR)0; (OS_ERR *)&err; /* Check ’err” */ OSTmrStart ((OS_TMR *)&MyTmr, (OS_ERR *)&err); /* Check ’err” */ } } Listing 12-3 Creating and Starting a timer - continued μC/OS-III will calculate the match value and index as follows: MatchValue = 12 + 10 Index into OSCfg_TmrWheel[] = 22 % 9 or, MatchValue = 22 Index into OSCfg_TickWheel[] = 4 The “second timer” will be inserted at the same table entry as shown in Figure 12-10, but sorted so that the timer with the least amount of time remaining before expiration is placed at the head of the list, and the timer with the longest to wait at the end. 206 Timer Management OSCfg_TmrWheel[] [0] [1] [2] [3] [4] [5] [6] [7] [8] 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 .NextPtr 0 0 0 0 .NbrEntriesMax .NbrEntries .FirstPtr 0 .PrevPtr .NextPtr 0 .PrevPtr 0 .Remain = 1 0 .Match = 13 .Match = 22 0 OS_TMR OS_TMR .Remain = 10 OSTmrTickCtr == 12 Figure 12-10 Inserting a second timer in the tick list When the timer task executes (see OS_TmrTask() in OS_TMR.C), it starts by incrementing OSTmrTickCtr and determines which table entry (i.e., spoke) it needs to update. Then, if there are timers in the list at this entry (i.e., .FirstPtr is not NULL), each OS_TMR is examined to determine whether the .Match value “matches” OSTmrTickCtr and, if so, the OS_TMR is removed from the list and OS_TmrTask() calls the timer callback function, assuming one was defined when the timer was created. The search through the list terminates as soon as OSTmrTickCtr does not match the timer’s .Match value. There is no point in looking any further in the list since the list is already sorted. Note that OS_TmrTask() does most of its work with the scheduler locked. However, because the list is sorted, and the search through the list terminates as soon as there no longer is a match, the critical section should be fairly short. 207 Chapter 12 12-5 SUMMARY Timers are down counters that perform an action when the counter reaches zero. The action is provided by the user through a callback function. μC/OS-III allows application code to create any number of timers (limited only by the amount of RAM available). The callback functions are executed in the context of the timer task with the scheduler locked. Keep callback functions as short and as fast as possible and do not have the callbacks make blocking calls. 208 Chapter 13 Resource Management This chapter will discuss services provided by μC/OS-III to manage shared resources. A shared resource is typically a variable (static or global), a data structure, table (in RAM), or registers in an I/O device. When protecting a shared resource it is preferred to use mutual exclusion semaphores, as will be described in this chapter. Other methods are also presented. Tasks can easily share data when all tasks exist in a single address space and can reference global variables, pointers, buffers, linked lists, ring buffers, etc. Although sharing data simplifies the exchange of information between tasks, it is important to ensure that each task has exclusive access to the data to avoid contention and data corruption. For example, when implementing a module that performs a simple time-of-day algorithm in software, the module obviously keeps track of hours, minutes and seconds. The TimeOfDay() task may appear as that shown in Listing 13-1. Imagine if this task was preempted by another task because an interrupt occurred, and, the other task was more important than the TimeOfDay() task) after setting the Minutes to 0. Now imagine what will happen if this higher priority task wants to know the current time from the time-of-day module. Since the Hours were not incremented prior to the interrupt, the higher-priority task will read the time incorrectly and, in this case, it will be incorrect by a whole hour. The code that updates variables for the TimeOfDay() task must treat all of the variables indivisibly (or atomically) whenever there is possible preemption. Time-of-day variables are considered shared resources and any code that accesses those variables must have exclusive access through what is called a critical section. μC/OS-III provides services to protect shared resources and enables the easy creation of critical sections. 209 Chapter 13 CPU_INT08U CPU_INT08U CPU_INT08U Hours; Minutes; Seconds; void TimeOfDay (void *p_arg) { OS_ERR err; (void)&p_arg; while (DEF_ON) { OSTimeDlyHMSM(0, 0, 1, 0, OS_OPT_TIME_HMSM_STRICT, &err); /* Examine “err” to make sure the call was successful */ Seconds++; if (Seconds > 59) { Seconds = 0; Minutes++; if (Minutes > 59) { Minutes = 0; Hours++; if (Hours > 23) { Hours = 0; } } } } } Listing 13-1 Faulty Time-Of-Day clock task The most common methods of obtaining exclusive access to shared resources and to create critical sections are: ■ disabling interrupts ■ disabling the scheduler ■ using semaphores ■ using mutual exclusion semaphores (a.k.a. a mutex) 210 Resource Management The mutual exclusion mechanism used depends on how fast the code will access a shared resource, as shown in Table 13-1. Resource Sharing Method When should you use? Disable/Enable Interrupts When access to shared resource is very quick (reading from or writing to few variables) and access is faster than μC/OS-III’s interrupt disable time. It is highly recommended to not use this method as it impacts interrupt latency. Locking/Unlocking the Scheduler When access time to the shared resource is longer than μC/OS-III’s interrupt disable time, but shorter than μC/OS-III’s scheduler lock time. Locking the scheduler has the same effect as making the task that locks the scheduler the highest-priority task. It is recommended not to use this method since it defeats the purpose of using μC/OS-III. However, it is a better method than disabling interrupts, as it does not impact interrupt latency. Semaphores When all tasks that need to access a shared resource do not have deadlines. This is because semaphores may cause unbounded priority inversions (described later). However, semaphore services are slightly faster (in execution time) than mutual-exclusion semaphores. Mutual Exclusion Semaphores This is the preferred method for accessing shared resources, especially if the tasks that need to access a shared resource have deadlines. Remember that μC/OS-III’s mutual exclusion semaphores have a built-in priority inheritance mechanism, which avoids unbounded priority inversions. However, mutual exclusion semaphore services are slightly slower (in execution time) than semaphores since the priority of the owner may need to be changed, which requires CPU processing. Table 13-1 Resource sharing 211 Chapter 13 13-1 DISABLE/ENABLE INTERRUPTS The easiest and fastest way to gain exclusive access to a shared resource is by disabling and enabling interrupts, as shown in the pseudo-code in Listing 13-2. Disable Interrupts; Access the resource; Enable Interrupts; Listing 13-2 Disabling and Enabling Interrupts μC/OS-III uses this technique (as do most, if not all, kernels) to access certain internal variables and data structures, ensuring that these variables and data structures are manipulated atomically. However, disabling and enabling interrupts are actually CPU-related functions rather than OS-related functions and functions in CPU-specific files are provided to accomplish this (see the CPU.H file of the processor being used). The services provided in the CPU module are called μC/CPU. Each different target CPU architecture has its own set of μC/CPU-related files. void OS_Function (void) { CPU_SR_ALLOC(); CPU_CRITICAL_ENTER(); Access the resource; CPU_CRITICAL_EXIT(); (1) (2) (3) (4) } Listing 13-3 Using CPU macros to disable and enable interrupts L13-3(1) 212 The CPU_SR_ALLOC() macro is required when the other two macros that disable/enable interrupts are used. This macro simply allocates storage for a local variable to hold the value of the current interrupt disable status of the CPU. If interrupts are already disabled we do not want to enable them upon exiting the critical section. Resource Management L13-3(2) CPU_CRITICAL_ENTER() saves the current state of the CPU interrupt disable flag(s) in the local variable allocated by CPU_SR_ALLOC() and disables all maskable interrupts. L13-3(3) The critical section of code is then accessed without fear of being changed by either an ISR or another task because interrupts are disabled. In other words, this operation is now atomic. L13-3(4) CPU_CRITICAL_EXIT() restores the previously saved interrupt disable status of the CPU from the local variable. CPU_CRITICAL_ENTER() and CPU_CRITICAL_EXIT() are always used in pairs. Interrupts should be disabled for as short a time as possible as disabling interrupts impacts the response of the system to interrupts. This is known as interrupt latency. Disabling and enabling is used only when changing or copying a few variables. Note, this is the only way that a task can share variables or data structures with an ISR. μC/CPU provides a way to actually measure interrupt latency. When using μC/OS-III, interrupts may be disabled for as much time as μC/OS-III does, without affecting interrupt latency. Obviously, it is important to know how long μC/OS-III disables interrupts, which depends on the CPU used. Although this method works, avoid disabling interrupts as it affects the responsiveness of the system to real-time events. 213 Chapter 13 13-2 LOCK/UNLOCK If the task does not share variables or data structures with an ISR, disable and enable μC/OS-III’s scheduler while accessing the resource, as shown in Listing 13-4. void OS_Function (void) { CPU_SR_ALLOC(); CPU_CRITICAL_ENTER(); Access the resource; CPU_CRITICAL_EXIT(); (1) (2) (3) (4) } Listing 13-4 Accessing a resource with the scheduler locked Using this method, two or more tasks share data without the possibility of contention. Note that while the scheduler is locked, interrupts are enabled and if an interrupt occurs while in the critical section, the ISR is executed immediately. At the end of the ISR, the kernel always returns to the interrupted task even if a higher priority task is made ready to run by the ISR. Since the ISR returns to the interrupted task, the behavior of the kernel is similar to that of a non-preemptive kernel (while the scheduler is locked). OSSchedLock() and OSSchedUnlock() can be nested up to 250 levels deep. The scheduler is invoked only when OSSchedUnlock() is called the same number of times the application called OSSchedLock(). After the scheduler is unlocked, μC/OS-III performs a context switch if a higher priority task is ready to run. μC/OS-III will not allow the user to make blocking calls when the scheduler is locked. If the application were able to make blocking calls, the application would most likely fail. Although this method works well, avoid disabling the scheduler as it defeats the purpose of having a preemptive kernel. Locking the scheduler makes the current task the highest priority task. 214 Resource Management 13-3 SEMAPHORES A semaphore originally was a mechanical signaling mechanism. The railroad industry used the device to provide a form of mutual exclusion for railroads tracks shared by more than one train. In this form, the semaphore signaled trains by closing a set of mechanical arms to block a train from a section of track that was currently in use. When the track became available, the arm would swing up and the waiting train would then proceed. The notion of using a semaphore in software as a means of synchronization was invented by the Dutch computer scientist Edgser Dijkstra in 1959. In computer software, a semaphore is a protocol mechanism offered by most multitasking kernels. Semaphores, originally used to control access to shared resources, now are used for synchronization as described in Chapter 14, “Synchronization” on page 251. However, it is useful to describe how semaphores can be used to share resources. The pitfalls of semaphores will be discussed in a later section. A semaphore was originally a “lock mechanism” and code acquired the key to this lock to continue execution. Acquiring the key means that the executing task has permission to enter the section of otherwise locked code. Entering a section of locked code causes the task to wait until the key becomes available. Typically, two types of semaphores exist: binary semaphores and counting semaphores. As its name implies, a binary semaphore can only take two values: 0 or 1. A counting semaphore allows for values between 0 and 255, 65,535, or 4,294,967,295, depending on whether the semaphore mechanism is implemented using 8, 16, or 32 bits, respectively. For μC/OS-III, the maximum value of a semaphore is determined by the data type OS_SEM_CTR (see OS_TYPE.H), which can be changed as needed (assuming μC/OS-III’s source code is available). Along with the semaphore’s value, μC/OS-III also keeps track of tasks waiting for the semaphore’s availability. Only tasks are allowed to use semaphores when semaphores are used for sharing resources; ISRs are not allowed. A semaphore is a kernel object defined by the OS_SEM data type, which is defined by the structure os_sem (see OS.H). The application can have any number of semaphores (limited only by the amount of RAM available). 215 Chapter 13 There are a number of operations the application is able to perform on semaphores, summarized in Table 13-2. In this chapter, only three functions used most often are discussed: OSSemCreate(), OSSemPend(), and OSSemPost(). Other functions are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. When semaphores are used for sharing resources, every semaphore function must be called from a task and never from an ISR. The same limitation does not apply when using semaphores for signaling, as described later in Chapter 13. Function Name Operation OSSemCreate() Create a semaphore. OSSemDel() Delete a semaphore. OSSemPend() Wait on a semaphore. OSSemPendAbort() Abort the wait on a semaphore. OSSemPost() Release or signal a semaphore. OSSemSet() Force the semaphore count to a desired value. Table 13-2 Semaphore API summary 216 Resource Management 13-3-1 BINARY SEMAPHORES A task that wants to acquire a resource must perform a Wait (or Pend) operation. If the semaphore is available (the semaphore value is greater than 0), the semaphore value is decremented, and the task continues execution (owning the resource). If the semaphore’s value is 0, the task performing a Wait on the semaphore is placed in a waiting list. μC/OS-III allows a timeout to be specified. If the semaphore is not available within a certain amount of time, the requesting task is made ready to run, and an error code (indicating that a timeout has occurred) is returned to the caller. A task releases a semaphore by performing a Signal (or Post) operation. If no task is waiting for the semaphore, the semaphore value is simply incremented. If there is at least one task waiting for the semaphore, the highest-priority task waiting on the semaphore is made ready to run, and the semaphore value is not incremented. If the readied task has a higher priority than the current task (the task releasing the semaphore), a context switch occurs and the higher-priority task resumes execution. The current task is suspended until it again becomes the highest-priority task that is ready to run. The operations described above are summarized using the pseudo-code shown in Listing 13-5. OS_SEM MySem; void main (void) { OS_ERR err; : : OSInit(&err); : OSSemCreate(&MySem, “My Semaphore”, 1, &err); /* Check “err” */ : /* Create task(s) */ : OSStart(&err); (void)err; } (1) (2) (3) (4) (5) Listing 13-5 Using a semaphore to access a shared resource 217 Chapter 13 L13-5(1) The application must declare a semaphore as a variable of type OS_SEM. This variable will be referenced by other semaphore services. L13-5(2) Create a semaphore semaphore allocated used by other tasks. main ()), however initialized before it is L13-5(3) Assign an ASCII name to the semaphore, which can be used by debuggers or μC/Probe to easily identify the semaphore. Storage for the ASCII characters is typically in ROM, which is typically more plentiful than RAM. If it is necessary to change the name of the semaphore at runtime, store the characters in an array in RAM and simply pass the address of the array to OSSemCreate(). Of course, the array must be NUL terminated. L13-5(4) Specify the initial value of the semaphore. Initialize the semaphore to 1 when the semaphore is used to access a single shared resource (as in this example). L13-5(5) OSSemCreate() returns an error code based on the outcome of the call. If all the arguments are valid, err will contain OS_ERR_NONE. Refer to the description of OSSemCreate() in Appendix A, “μC/OS-III API Reference Manual” on page 375 for a list of other error codes and their meaning. 218 by calling OSSemCreate() and pass the address to the in (1). The semaphore must be created before it can be Here, the semaphore is initialized in startup code (i.e., it could also be initialized by a task (but it must be used). Resource Management void Task1 (void *p_arg) { OS_ERR err; CPU_TS ts; while (DEF_ON) { : OSSemPend(&MySem, 0, OS_OPT_PEND_BLOCKING, &ts, &err); switch (err) { case OS_ERR_NONE: Access Shared Resource; OSSemPost(&MySem, OS_OPT_POST_1, &err); /* Check “err” */ break; (6) (7) (8) (9) (10) (11) (12) (13) (14) case OS_ERR_PEND_ABORT: /* The pend was aborted by another task break; */ case OS_ERR_OBJ_DEL: /* The semaphore was deleted break; */ default: /* Other errors */ } : } } Listing 13-6 Using a semaphore to access a shared resource L13-6(6) The task pends (or waits) on the semaphore by calling OSSemPend(). The application must specify the desired semaphore to wait upon, and the semaphore must have been previously created. L13-6(7) The next argument is a timeout specified in number of clock ticks. The actual timeout depends on the tick rate. If the tick rate (see OS_CFG_APP.H) is set to 1000, a timeout of 10 ticks represents 10 milliseconds. Specifying a timeout of zero (0) means waiting forever for the semaphore. 219 Chapter 13 L13-6(8) The third argument specifies how to wait. There are two options: OS_OPT_PEND_BLOCKING and OS_OPT_PEND_NON_BLOCKING. The blocking option means that if the semaphore is not available, the task calling OSSemPend() will wait until the semaphore is posted or until the timeout expires. The non-blocking option indicates that if the semaphore is not available, OSSemPend() will return immediately and not wait. This last option is rarely used when using a semaphore to protect a shared resource. L13-6(9) When the semaphore is posted, μC/OS-III reads a “timestamp” and returns this timestamp when OSSemPend() returns. This feature allows the application to know “when” the post happened and the semaphore was released. At this point, OS_TS_GET() is read to get the current timestamp and compute the difference, indicating the length of the wait. L13-6(10) OSSemPend() returns an error code based on the outcome of the call. If the call is successful, err will contain OS_ERR_NONE. If not, the error code will indicate the reason for the error. See Appendix A, “μC/OS-III API Reference Manual” on page 375 for a list of possible error code for OSSemPend(). Checking for error return values is important since other tasks might delete or otherwise abort the pend. However, it is not a recommended practice to delete kernel objects at run time as the action may cause serious problems. L13-6(11) The resource can be accessed when OSSemPend() returns, if there are no errors. L13-6(12) When finished accessing the resource, simply call OSSemPost() and specify the semaphore to be released. L13-6(13) OS_OPT_POST_1 indicates that the semaphore is signaling a single task, if there are many tasks waiting on the semaphore. In fact, always specify this option when a semaphore is used to access a shared resource. L13-6(14) As with most μC/OS-III functions, specify the address of a variable that will receive an error message from the call. 220 Resource Management void Task2 (void *p_arg) { OS_ERR CPU_TS err; ts; while (DEF_ON) { : OSSemPend(&MySem, 0, (15) OS_OPT_PEND_BLOCKING, &ts, &err); switch (err) { case OS_ERR_NONE: Access Shared Resource; OSSemPost(&MySem, OS_OPT_POST_1, &err); /* Check “err” */ break; case OS_ERR_PEND_ABORT: /* The pend was aborted by another task break; */ case OS_ERR_OBJ_DEL: /* The semaphore was deleted break; */ default: /* Other errors */ } : } } Listing 13-7 Using a semaphore to access a shared resource L13-7(15) Another task wanting to access the shared resource needs to use the same procedure to access the shared resource. 221 Chapter 13 Semaphores are especially useful when tasks share I/O devices. Imagine what would happen if two tasks were allowed to send characters to a printer at the same time. The printer would contain interleaved data from each task. For instance, the printout from Task 1 printing “I am Task 1,” and Task 2 printing “I am Task 2,” could result in “I Ia amm T Tasask k1 2”. In this case, use a semaphore and initialize it to 1 (i.e., a binary semaphore). The rule is simple: to access the printer each task must first obtain the resource’s semaphore. Figure 13-1 shows tasks competing for a semaphore to gain exclusive access to the printer. Note that a key, indicating that each task must obtain this key to use the printer, represents the semaphore symbolically. Task 1 OSSemPend() Access Printer OSSemPost() Printer OSSemPend() AccessPrinter OSSemPost() Task 2 Figure 13-1 Using a semaphore to access a printer The above example implies that each task knows about the existence of the semaphore to access the resource. It is almost always better to encapsulate the critical section and its protection mechanism. Each task would therefore not know that it is acquiring a semaphore when accessing the resource. For example, an RS-232C port is used by multiple tasks to send commands and receive responses from a device connected at the other end as shown in Figure 13-2. 222 Resource Management &2000RGXOH 7DVN &RPP6HQG&PG 'ULYHU 56& &RPP6HQG&PG 7DVN 266HP3HQG 266HP3RVW 6HPDSKRUH Figure 13-2 Hiding a semaphore from a task The function CommSendCmd() is called with three arguments: the ASCII string containing the command, a pointer to the response string from the device, and finally, a timeout in case the device does not respond within a certain amount of time. The pseudo-code for this function is shown in Listing 13-8. APP_ERR CommSendCmd (CPU_CHAR CPU_CHAR OS_TICK *cmd, *response, timeout) { Acquire serial port’s semaphore; Send “cmd” to device; Wait for response with “timeout”; if (timed out) { Release serial port’s semaphore; return (error code); } else { Release serial port’s semaphore; return (no error); } } Listing 13-8 Encapsulating the use of a semaphore 223 Chapter 13 Each task that needs to send a command to the device must call this function. The semaphore is assumed to be initialized to 1 (i.e., available) by the communication driver initialization routine. The first task that calls CommSendCmd() acquires the semaphore, proceeds to send the command, and waits for a response. If another task attempts to send a command while the port is busy, this second task is suspended until the semaphore is released. The second task appears simply to have made a call to a normal function that will not return until the function performs its duty. When the semaphore is released by the first task, the second task acquires the semaphore and is allowed to use the RS-232C port. 13-3-2 COUNTING SEMAPHORES A counting semaphore is used when elements of a resource can be used by more than one task at the same time. For example, a counting semaphore is used in the management of a buffer pool, as shown in Figure 13-3. Assume that the buffer pool initially contains 10 buffers. A task obtains a buffer from the buffer manager by calling BufReq(). When the buffer is no longer needed, the task returns the buffer to the buffer manager by calling BufRel(). The pseudo-code for these functions is shown in Listing 13-9. The buffer manager satisfies the first 10 buffer requests because the semaphore is initialized to 10. When all buffers are used, a task requesting a buffer is suspended until a buffer becomes available. Use μC/OS-III’s OSMemGet() and OSMemPut() (see Chapter 17, “Memory Management” on page 323) to obtain a buffer from the buffer pool. When a task is finished with the buffer it acquired, the task calls BufRel() to return the buffer to the buffer manager and the buffer is inserted into the linked list before the semaphore is signaled. By encapsulating the interface to the buffer manager in BufReq() and BufRel(), the caller does not need to be concerned with actual implementation details. 224 Resource Management 7DVN %XI5HT 7DVN %XIIHU 0DQDJHU %XI5HO RI %XI)UHH/LVW3WU 1H[W3WU 1H[W3WU 1H[W3WU  Figure 13-3 Using a counting semaphore BUF { *BufReq (void) BUF *ptr; Wait on semaphore; ptr = OSMemGet(...) ; return (ptr); /* Get a buffer */ } void BufRel (BUF *ptr) { OSMemPut(..., (void *)ptr, ...); Signal semaphore; } /* Return the buffer */ Listing 13-9 Buffer management using a semaphore. 225 Chapter 13 Note that the details of creating the memory partition are removed since this is discussed in Chapter 17, “Memory Management” on page 323. The semaphore is used here to extend the memory management capabilities of μC/OS-III, and to provide it with a blocking mechanism. However, only tasks can make BufReq() and BufRel() calls. 13-3-3 NOTES ON SEMAPHORES Using a semaphore to access a shared resource does not increase interrupt latency. If an ISR or the current task makes a higher priority task ready to run while accessing shared data, the higher priority task executes immediately. An application may have as many semaphores as required to protect a variety of different resources. For example, one semaphore may be used to access a shared display, another to access a shared printer, another for shared data structures, and another to protect a pool of buffers, etc. However, it is preferable to use semaphores to protect access to I/O devices rather than memory locations. Semaphores are often overused. The use of a semaphore to access a simple shared variable is overkill in most situations. The overhead involved in acquiring and releasing the semaphore consumes valuable CPU time. Perform the job more efficiently by disabling and enabling interrupts, however there is an indirect cost to disabling interrupts: even higher priority tasks that do not share the specific resource are blocked from using the CPU. Suppose, for instance, that two tasks share a 32-bit integer variable. The first task increments the variable, while the second task clears it. When considering how long a processor takes to perform either operation, it is easy to see that a semaphore is not required to gain exclusive access to the variable. Each task simply needs to disable interrupts before performing its operation on the variable and enable interrupts when the operation is complete. A semaphore should be used if the variable is a floating-point variable and the microprocessor does not support hardware floating-point operations. In this case, the time involved in processing the floating-point variable may affect interrupt latency if interrupts are disabled. Semaphores are subject to a serious problem in real-time systems called priority inversion, which is described in section 13-3-5 “Priority Inversions” on page 232. 226 Resource Management 13-3-4 SEMAPHORE INTERNALS (FOR RESOURCE SHARING) As previously mentioned, a semaphore is a kernel object as defined by the OS_SEM data type, which is derived from the structure os_sem (see OS.H) as shown in Listing 13-10. The services provided by μC/OS-III to manage semaphores are implemented in the file OS_SEM.C. μC/OS-III licensees have access to the source code. In this case, semaphore services are enabled at compile time by setting the configuration constant OS_CFG_SEM_EN to 1 in OS_CFG.H. typedef struct struct os_sem { OS_OBJ_TYPE CPU_CHAR OS_PEND_LIST OS_SEM_CTR CPU_TS }; os_sem OS_SEM; (1) Type; *NamePtr; PendList; Ctr; TS; (2) (3) (4) (5) (6) Listing 13-10 OS_SEM data type L13-10(1) In μC/OS-III, all structures are given a data type. All data types start with “OS_” and are uppercase. When a semaphore is declared, simply use OS_SEM as the data type of the variable used to declare the semaphore. L13-10(2) The structure starts with a “Type” field, which allows it to be recognized by μC/OS-III as a semaphore. Other kernel objects will also have a “.Type” as the first member of the structure. If a function is passed a kernel object, μC/OS-III will confirm that it is being passed the proper data type. For example, if passing a message queue (OS_Q) to a semaphore service (for example OSSemPend()), μC/OS-III will recognize that an invalid object was passed, and return an error code accordingly. L13-10(3) Each kernel object can be given a name for easier recognition by debuggers or μC/Probe. This member is simply a pointer to an ASCII string, which is assumed to be NUL terminated. 227 Chapter 13 L13-10(4) Since it is possible for multiple tasks to wait (or pend) on a semaphore, the semaphore object contains a pend list as described in Chapter 10, “Pend Lists (or Wait Lists)” on page 177. L13-10(5) A semaphore contains a counter. As explained above, the counter can be implemented as either an 8-, 16- or 32-bit value, depending on how the data type OS_SEM_CTR is declared in OS_TYPE.H. μC/OS-III does not make a distinction between binary and counting semaphores. The distinction is made when the semaphore is created. If creating a semaphore with an initial value of 1, it is a binary semaphore. When creating a semaphore with a value > 1, it is a counting semaphore. In the next chapter, we discover that a semaphore is more often used as a signaling mechanism and therefore, the semaphore counter is initialized to zero. L13-10(6) A semaphore contains a timestamp used to indicate the last time the semaphore was posted. μC/OS-III assumes the presence of a free-running counter that allows the application to make time measurements. When the semaphore is posted, the free-running counter is read and the value is placed in this field, which is returned when OSSemPend() is called. The value of this field is more useful when a semaphore is used as a signaling mechanism (see Chapter 14, “Synchronization” on page 251), as opposed to a resource-sharing mechanism. Even if the user understands the internals of the OS_SEM data type, the application code should never access any of the fields in this data structure directly. Instead, always use the APIs provided with μC/OS-III. As previously mentioned, semaphores must be created before they can be used by an application. A task waits on a semaphore before accessing a shared resource by calling OSSemPend() as shown in Listing 13-11 (see Appendix A, “μC/OS-III API Reference Manual” on page 375 for details regarding the arguments). 228 Resource Management OS_SEM MySem; void MyTask (void *p_arg) { OS_ERR CPU_TS err; ts; : while (DEF_ON) { : OSSemPend(&MySem, 10, OS_OPT_PEND_BLOCKING, &ts, &err); : /* Check “err” */ : OSSemPost(&MySem, OS_OPT_POST_1, &err); /* Check “err” */ : : } /* (1) Pointer to semaphore /* Wait up until this time for the semaphore /* Option(s) /* Returned timestamp of when sem. was released /* Pointer to Error returned */ */ */ */ */ /* (2) */ /* (3) Pointer to semaphore /* Option(s) … always OS_OPT_POST_1 /* Pointer to Error returned */ */ */ } Listing 13-11 Pending on and Posting to a Semaphore L13-11(1) When called, OSSemPend() starts by checking the arguments passed to this function to make sure they have valid values. If the semaphore counter (.Ctr of OS_SEM) is greater than zero, the counter is decremented and OSSemPend() returns. If OSSemPend() returns without error, then the task now owns the shared resource. When the semaphore counter is zero, this indicates that another task owns the semaphore, and the calling task may need to wait for the semaphore to be released. If specifying OS_OPT_PEND_NON_BLOCKING as the option (the application does not want the task to block), OSSemPend() returns immediately to the caller and the returned error code indicates that the semaphore is unavailable. Use this option if the task does not want to wait for the resource to be available, and would prefer to do something else and check back later. 229 Chapter 13 If specifying the OS_OPT_PEND_BLOCKING option, the calling task will be inserted in the list of tasks waiting for the semaphore to become available. The task is inserted in the list by priority order and therefore, the highest priority task waiting on the semaphore is at the beginning of the list. If specifying a non-zero timeout, the task will also be inserted in the tick list. A zero value for a timeout indicates that the user is willing to wait forever for the semaphore to be released. Most of the time, specify an infinite timeout when using the semaphore in resource sharing. Adding a timeout may temporarily break a deadlock, however, there are better ways of preventing deadlock at the application level (e.g., never hold more than one semaphore at the same time; resource ordering; etc.). The scheduler is called since the current task is no longer able to run (it is waiting for the semaphore to be released). The scheduler will then run the next highest-priority task that is ready to run. When the semaphore is released and the task that called OSSemPend() is again the highest-priority task, μC/OS-III examines the task status to determine the reason why OSSemPend() is returning to its caller. The possibilities are: 1) The semaphore was given to the waiting task 2) The pend was aborted by another task 3) The semaphore was not posted within the specified timeout 4) The semaphore was deleted When OSSemPend() returns, the caller is notified of the above outcome through an appropriate error code. 230 Resource Management L13-11(2) If OSSemPend() returns with err set to OS_ERR_NONE, assume that you now have access to the resource. If err contains anything else, OSSemPend() either timed out (if the timeout argument was non-zero), the pend was aborted by another task, or the semaphore was deleted by another task. It is always important to examine the returned error code and not assume that everything went well. L13-11(3) When the task is finished accessing the resource, it needs to call OSSemPost() and specify the same semaphore. Again, OSSemPost() starts by checking the arguments passed to this function to make sure there are valid values. OSSemPost() then calls OS_TS_GET() to obtain the current timestamp so it can place that information in the semaphore to be used by OSSemPend(). OSSemPost() checks to see if any tasks are waiting for the semaphore. If not, OSSemPost() simply increments p_sem->Ctr, saves the timestamp in the semaphore, and returns. If there are tasks waiting for the semaphore to be released, OSSemPost() extracts the highest-priority task waiting for the semaphore. This is a fast operation as the pend list is sorted by priority order. When calling OSSemPost(), it is possible to specify as an option to not call the scheduler. This means that the post is performed, but the scheduler is not called even if a higher priority task waits for the semaphore to be released. This allows the calling task to perform other post functions (if needed) and make all posts take effect simultaneously without the possibility of context switching in between each post. 231 Chapter 13 13-3-5 PRIORITY INVERSIONS Priority inversion is a problem in real-time systems, and occurs only when using a priority-based preemptive kernel. Figure 13-4 illustrates a priority-inversion scenario. Task H (high priority) has a higher priority than Task M (medium priority), which in turn has a higher priority than Task L (low priority). 8QERXQGHG 3ULRULW\ ,QYHUVLRQ 7DVN+   7DVN+ 3UHHPSWV 7DVN/ 7DVN0  6HPDSKRUH 2ZQHG E\7DVN/  7DVN0 3UHHPSWV 7DVN/  7DVN/     7DVN/ 5HOHDVHV 6HPDSKRUH  7DVN0 'RQH   7DVN/ JHWV6HPDSKRUH  Figure 13-4 Unbounded priority Inversion F13-4(1) Task H and Task M are both waiting for an event to occur and Task L is executing. F13-4(2) At some point, Task L acquires a semaphore, which it needs before it can access a shared resource. F13-4(3) Task L performs operations on the acquired resource. F13-4(4) The event that Task H was waiting for occurs, and the kernel suspends Task L and start executing Task H since Task H has a higher priority. F13-4(5) Task H performs computations based on the event it just received. 232 Resource Management F13-4(6) Task H now wants to access the resource that Task L currently owns (i.e., it attempts to get the semaphore that Task L owns). Because Task L owns the resource, Task H is placed in a list of tasks waiting for the semaphore to be free. F13-4(7) Task L is resumed and continues to access the shared resource. F13-4(8) Task L is preempted by Task M since the event that Task M was waiting for occurred. F13-4(9) Task M handles the event. F13-4(10) When Task M completes, the kernel relinquishes the CPU back to Task L. F13-4(11) Task L continues accessing the resource. F13-4(12) Task L finally finishes working with the resource and releases the semaphore. At this point, the kernel knows that a higher-priority task is waiting for the semaphore, and a context switch takes place to resume Task H. F13-4(13) Task H has the semaphore and can access the shared resource. So, what happened here is that the priority of Task H has been reduced to that of Task L since it waited for the resource that Task L owned. The trouble begins when Task M preempted Task L, further delaying the execution of Task H. This is called an unbounded priority inversion. It is unbounded because any medium priority can extend the time Task H has to wait for the resource. Technically, if all medium-priority tasks have known worst-case periodic behavior and bounded execution times, the priority inversion time is computable. This process, however, may be tedious and would need to be revised every time the medium priority tasks change. This situation can be corrected by raising the priority of Task L, only during the time it takes to access the resource, and restore the original priority level when the task is finished. The priority of Task L should be raised up to the priority of Task H. μC/OS-III contains a special type of semaphore that does just that called a mutual-exclusion semaphore. 233 Chapter 13 13-4 MUTUAL EXCLUSION SEMAPHORES (MUTEX) μC/OS-III supports a special type of binary semaphore called a mutual exclusion semaphore (also known as a mutex) that eliminates unbounded priority inversions. Figure 13-5 shows how priority inversions are bounded using a Mutex. $FFHVV 7R 6KDUHG 5HVRXUFH 7DVN+ UHOHDVHV WKH 0XWH[  7DVN+  7DVN+ SUHHPSWV 7DVN/ 7DVN0   7DVN/ 0XWH[ GRQHZLWK RZQHG 0XWH[ E\ —&26,,, 7DVN/ ORZHUV —&26,,, SULRULW\ UDLVHG RI SULRULW\ 7DVN/ RI 7DVN+ 7DVN/ UHVXPHV WRWKDWRI DQGQRZRZQV 7DVN+ 0XWH[  7DVN/    7DVN+ 'RQH     7DVN/ JHWV 0XWH[  Figure 13-5 Using a mutex to share a resource F13-5(1) Task H and Task M are both waiting for an event to occur and Task L is executing. F13-5(2) At some point, Task L acquires a mutex, which it needs before it is able to access a shared resource. F13-5(3) Task L performs operations on the acquired resource. F13-5(4) The event that Task H waited for occurs and the kernel suspends Task L and begins executing Task H since Task H has a higher priority. 234 Resource Management F13-5(5) Task H performs computations based on the event it just received. F13-5(6) Task H now wants to access the resource that Task L currently owns (i.e., it attempts to get the mutex from Task L). Given that Task L owns the resource, μC/OS-III raises the priority of Task L to the same priority as Task H to allow Task L to finish with the resource and prevent Task L from being preempted by medium-priority tasks. F13-5(7) Task L continues accessing the resource, however it now does so while it is running at the same priority as Task H. Note that Task H is not actually running since it is waiting for Task L to release the mutex. In other words, Task H is in the mutex wait list. F13-5(8) Task L finishes working with the resource and releases the mutex. μC/OS-III notices that Task L was raised in priority and thus lowers Task L to its original priority. After doing so, μC/OS-III gives the mutex to Task H, which was waiting for the mutex to be released. F13-5(9) Task H now has the mutex and can access the shared resource. F13-5(10) Task H is finished accessing the shared resource, and frees up the mutex. F13-5(11) There are no higher-priority tasks to execute, therefore Task H continues execution. F13-5(12) Task H completes and decides to wait for an event to occur. At this point, μC/OS-III resumes Task M, which was made ready to run while Task H or Task L were executing. F13-5(13) Task M executes. Note that there is no priority inversion, only resource sharing. Of course, the faster Task L accesses the shared resource and frees up the mutex, the better. μC/OS-III implements full-priority inheritance and therefore if a higher priority requests the resource, the priority of the owner task will be raised to the priority of the new requestor. 235 Chapter 13 A mutex is a kernel object defined by the OS_MUTEX data type, which is derived from the structure os_mutex (see OS.H). An application may have an unlimited number of mutexes (limited only by the RAM available). Only tasks are allowed to use mutual exclusion semaphores (ISRs are not allowed). μC/OS-III enables the user to nest ownership of mutexes. If a task owns a mutex, it can own the same mutex up to 250 times. The owner must release the mutex an equivalent number of times. In several cases, an application may not be immediately aware that it called OSMutexPend() multiple times, especially if the mutex is acquired again by calling a function as shown in Listing 13-12. OS_MUTEX SOME_STRUCT MyMutex; MySharedResource; void MyTask (void *p_arg) { OS_ERR err; CPU_TS ts; : while (DEF_ON) { OSMutexPend((OS_MUTEX *)&MyMutex, (OS_TICK )0, (OS_OPT )OS_OPT_PEND_BLOCKING, (CPU_TS *)&ts, (OS_ERR *)&err); /* Check ’err” /* Acquire shared resource if no error MyLibFunction(); OSMutexPost((OS_MUTEX *)&MyMutex, (OS_OPT )OS_OPT_POST_NONE, (OS_ERR *)&err); /* Check “err” } } 236 (1) */ */ (2) (3) (7) */ Resource Management void { MyLibFunction (void) OS_ERR CPU_TS err; ts; OSMutexPend((OS_MUTEX *)&MyMutex, (OS_TICK )0, (4) (OS_OPT )OS_OPT_PEND_BLOCKING, (CPU_TS *)&ts, (OS_ERR *)&err); /* Check “err” */ /* Access shared resource if no error */ OSMutexPost((OS_MUTEX *)&MyMutex, (OS_OPT )OS_OPT_POST_NONE, (OS_ERR *)&err); /* Check “err” */ (5) (6) } Listing 13-12 Nesting calls to OSMutexPend() L13-12(1) A task starts by pending on a mutex to access shared resources. OSMutexPend() sets a nesting counter to 1. L13-12(2) Check the error return MySharedResource. L13-12(3) A function is called that will perform additional work. L13-12(4) The designer of MyLibFunction() knows that, to access MySharedResource, it must acquire the mutex. Since the calling task already owns the mutex, this operation should not be necessary. However, MyLibFunction() could have been called by yet another function that might not need access to MySharedResource. μC/OS-III allows nested mutex pends, so this is not a problem. The mutex nesting counter is thus incremented to 2. L13-12(5) MyLibFunction() can access the shared resource. value. If no errors exist, MyTask() owns 237 Chapter 13 L13-12(6) The mutex is released and the nesting counter is decremented back to 1. Since this indicates that the mutex is still owned by the same task, nothing further needs to be done, and OSMutexPost() simply returns. MyLibFunction() returns to its caller. L13-12(7) The mutex is released again and, this time, the nesting counter is decremented back to 0 indicating that other tasks can now acquire the mutex. Always check the return value of OSMutexPend() (and any kernel call) to ensure that the function returned because you properly obtained the mutex, and not because the return from OSMutexPend() was caused by the mutex being deleted, or because another task called OSMutexPendAbort() on this mutex. As a general rule, do not make function calls in critical sections. All mutual exclusion semaphore calls should be in the leaf nodes of the source code (e.g., in the low level drivers that actually touches real hardware or in other reentrant function libraries). There are a number of operations that can be performed on a mutex, as summarized in Table 13-3. However, in this chapter, we will only discuss the three functions that most often used: OSMutexCreate(), OSMutexPend(), and OSMutexPost(). Other functions are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. Function Name Operation OSMutexCreate() Create a mutex. OSMutexDel() Delete a mutex. OSMutexPend() Wait on a mutex. OSMutexPendAbort() Abort the wait on a mutex. OSMutexPost() Release a mutex. Table 13-3 Mutex API summary 238 Resource Management 13-4-1 MUTUAL EXCLUSION SEMAPHORE INTERNALS A mutex is a kernel object defined by the OS_MUTEX data type, which is derived from the structure os_mutex (see OS.H) as shown in Listing 13-13: typedef struct os_mutex struct os_mutex { OS_OBJ_TYPE CPU_CHAR OS_PEND_LIST OS_TCB OS_PRIO OS_NESTING_CTR CPU_TS }; OS_MUTEX; Type; *NamePtr; PendList; *OwnerTCBPtr; OwnerOriginalPrio; OwnerNestingCtr; TS; (1) (2) (3) (4) (5) (6) (7) (8) Listing 13-13 OS_MUTEX data type L13-13(1) In μC/OS-III, all structures are given a data type. All data types begin with “OS_” and are uppercase. When a mutex is declared, simply use OS_MUTEX as the data type of the variable used to declare the mutex. L13-13(2) The structure starts with a “Type” field, which allows it to be recognized by μC/OS-III as a mutex. Other kernel objects may also have a “.Type” as the first member of the structure. If a function is passed a kernel object, μC/OS-III will be able to confirm that it is being passed the proper data type. For example, if passing a message queue (OS_Q) to a mutex service (for example OSMutexPend()), μC/OS-III will recognize that the application passed an invalid object and return an error code accordingly. L13-13(3) Each kernel object can be given a name to make them easier to recognize by debuggers or μC/Probe. This member is simply a pointer to an ASCII string, which is assumed to be NUL terminated. L13-13(4) Because it is possible for multiple tasks to wait (or pend on a mutex), the mutex object contains a pend list as described in Chapter 10, “Pend Lists (or Wait Lists)” on page 177. 239 Chapter 13 L13-13(5) If the mutex is owned by a task, it will point to the OS_TCB of that task. L13-13(6) If the mutex is owned by a task, this field contains the “original” priority of the task that owns the mutex. This field is required in case the priority of the task must be raised to a higher priority to prevent unbounded priority inversions. L13-13(7) μC/OS-III allows a task to “acquire” the same mutex multiple times. In order for the mutex to be released, the owner must release the mutex the same number of times that it was acquired. Nesting can be performed up to 250-levels deep. L13-13(8) A mutex contains a timestamp, used to indicate the last time it was released. μC/OS-III assumes the presence of a free-running counter that allows applications to make time measurements. When the mutex is released, the free-running counter is read and the value is placed in this field, which is returned when OSMutexPend() returns. Application code should never access any of the fields in this data structure directly. Instead, always use the APIs provided with μC/OS-III. A mutual exclusion semaphore (mutex) must be created before it can be used by an application. Listing 13-14 shows how to create a mutex. OS_MUTEX void MyMutex; (1) MyTask (void *p_arg) { OS_ERR err; : : OSMutexCreate(&MyMutex, (2) “My Mutex”, (3) &err); (4) /* Check “err” */ : : } Listing 13-14 Creating a mutex 240 Resource Management L13-14(1) The application must declare a variable of type OS_MUTEX. This variable will be referenced by other mutex services. L13-14(2) Create a mutex by calling OSMutexCreate() and pass the address to the mutex allocated in (1). L13-14(3) Assign an ASCII name to the mutex, which can be used by debuggers or μC/Probe to easily identify this mutex. There are no practical limits to the length of the name since μC/OS-III stores a pointer to the ASCII string, and not to the actual characters that makes up the string. L13-14(4) OSMutexCreate() returns an error code based on the outcome of the call. If all the arguments are valid, err will contain OS_ERR_NONE. Note that since a mutex is always a binary semaphore, there is no need to initialize a mutex counter. A task waits on a mutual exclusion semaphore before accessing a shared resource by calling OSMutexPend() as shown in Listing 13-15 (see Appendix A, “μC/OS-III API Reference Manual” on page 375 for details regarding the arguments). 241 Chapter 13 OS_MUTEX MyMutex; void MyTask (void *p_arg) { OS_ERR err; CPU_TS ts; : while (DEF_ON) { : OSMutexPend(&MyMutex, /* (1) Pointer to mutex */ /* Wait up until this time for the mutex */ OS_OPT_PEND_BLOCKING, /* Option(s) */ &ts, /* Timestamp of when mutex was released */ &err); /* Pointer to Error returned */ 10, : /* Check “err” (2) */ : OSMutexPost(&MyMutex, /* (3) Pointer to mutex */ /* */ OS_OPT_POST_NONE, &err); /* Check “err” Pointer to Error returned */ : : } } Listing 13-15 Pending (or waiting) on a Mutual Exclusion Semaphore L13-15(1) When called, OSMutexPend() starts by checking the arguments passed to this function to make sure they have valid values. If the mutex is available, OSMutexPend() assumes the calling task is now the owner of the mutex and stores a pointer to the task’s OS_TCB in p_mutex->OwnerTCPPtr, saves the priority of the task in p_mutex->OwnerOriginalPrio, and sets a mutex nesting counter to 1. OSMutexPend() then returns to its caller with an error code of OS_ERR_NONE. If the task that calls OSMutexPend() already owns the mutex, OSMutexPend() simply increments a nesting counter. Applications can nest calls to OSMutexPend() up to 250-levels deep. In this case, the error returned will indicate OS_ERR_MUTEX_OWNER. 242 Resource Management If the mutex is already owned by another task and OS_OPT_PEND_NON_BLOCKING is specified, OSMutexPend() returns since the task is not willing to wait for the mutex to be released by its owner. If the mutex is owned by a lower-priority task, μC/OS-III will raise the priority of the owner to match the priority of the current task. If specifying OS_OPT_PEND_BLOCKING as the option, the calling task will be inserted in the list of tasks waiting for the mutex to be available. The task is inserted in the list by priority order and the highest priority task waiting on the mutex is at the beginning of the list. If further specifying a non-zero timeout, the task will also be inserted in the tick list. A zero value for a timeout indicates a willingness to wait forever for the mutex to be released. The scheduler is then called since the current task is no longer able to run (it is waiting for the mutex to be released). The scheduler will then run the next highest-priority task that is ready to run. When the mutex is finally released and the task that called OSMutexPend() is again the highest-priority task, a task status is examined to determine the reason why OSMutexPend() is returning to its caller. The possibilities are: 1) The mutex was given to the waiting task 2) The pend was aborted by another task 3) The mutex was not posted within the specified timeout 4) The mutex was deleted When OSMutexPend() returns, the caller is notified of the outcome through an appropriate error code. 243 Chapter 13 L13-15(2) If OSMutexPend() returns with err set to OS_ERR_NONE, assume that the calling task now owns the resource and can proceed with accessing it. If err contains anything else, then OSMutexPend() either timed out (if the timeout argument was non-zero), the pend was aborted by another task, or the mutex was deleted by another task. It is always important to examine returned error codes and not assume everything went as planned. If “err” is OS_ERR_NESTING_OWNER, then the caller attempted to pend on the same mutex. L13-15(3) When your task is finished accessing the resource, it must call OSMutexPost() and specify the same mutex. Again, OSMutexPost() starts by checking the arguments passed to this function to make sure they contain valid values. OSMutexPost() now calls OS_TS_GET() to obtain the current timestamp and place that information in the mutex, which will be used by OSMutexPend(). OSMutexPost() decrements the nesting counter and, if still non-zero, OSMutexPost() returns to the caller. In this case, the current owner has not fully released the mutex. The error code will indicate OS_ERR_MUTEX_NESTING. If there are no tasks waiting for the mutex, OSMutexPost() sets p_mutex->OwnerTCBPtr to a NULL pointer and clears the mutex nesting counter. If μC/OS-III had to raise the priority of the mutex owner, it is returned to its original priority at this time. The highest-priority task waiting on the mutex is then extracted from the pend list and given the mutex. This is a fast operation since the pend list is sorted by priority. The scheduler is called to see if the new mutex owner has a higher priority than the current task. If so, μC/OS-III will switch context to the new mutex owner. You should note that you should only acquire one mutex at a time. In fact, it’s highly recommended that when you acquire a mutex, you don’t acquire any other kernel objects. 244 Resource Management 13-5 SHOULD YOU USE A SEMAPHORE INSTEAD OF A MUTEX? A semaphore can be used instead of a mutex if none of the tasks competing for the shared resource have deadlines to be satisfied. However, if there are deadlines to meet, you should use a mutex prior to accessing shared resources. Semaphores are subject to unbounded priority inversions, while mutex are not. 13-6 DEADLOCKS (OR DEADLY EMBRACE) A deadlock, also called a deadly embrace, is a situation in which two tasks are each unknowingly waiting for resources held by the other. Assume Task T1 has exclusive access to Resource R1 and Task T2 has exclusive access to Resource R2 as shown in the pseudo-code of Listing 13-16. void T1 (void *p_arg) { while (DEF_ON) { Wait for event to occur; Acquire M1; Access R1; : : \-------- Interrupt! : : Acquire M2; Access R2; } } (1) (2) (3) (4) (8) (9) 245 Chapter 13 void { T2 (void *p_arg) while (DEF_ON) { Wait for event to occur; Acquire M2; Access : : (5) (6) R2; Acquire M1; Access R1; (7) } } Listing 13-16 Deadlock problem L13-16(1) Assume that the event that task T1 is waiting for occurs and T1 is now the highest priority task that must execute. L13-16(2) Task T1 executes and acquires M1. L13-16(3) Resource R1 is accessed. L13-16(4) An interrupt occurs causing the CPU to switch to task T2 as T2 is now the highest-priority task. Actually, this could be a blocking call when the task is suspended and the CPU is given to another task. L13-16(5) The ISR is the event that task T2 was waiting for and therefore T2 resumes execution. L13-16(6) Task T2 acquires mutex M2 and is able to access resource R2. L13-16(7) Task T2 tries to acquire mutex M1, but μC/OS-III knows that mutex M1 is owned by another task. L13-16(8) μC/OS-III switches back to task T1 because Task T2 can no longer continue. It needs mutex M1 to access resource R1. L13-16(9) Task T1 now tries to access mutex M2 but, unfortunately, mutex M2 is owned by task T2. At this point, the two tasks are deadlocked. 246 Resource Management Techniques used to avoid deadlocks are for tasks to: ■ Never acquire more than one mutex at a time ■ Never acquire a mutex directly (i.e., let them be hidden inside drivers and reentrant library calls) ■ Acquire all resources before proceeding ■ Always acquire resources in the same order μC/OS-III allows the calling task to specify a timeout when acquiring a semaphore or a mutex. This feature allows a deadlock to be broken, but the same deadlock may then recur later, or many times later. If the semaphore or mutex is not available within a certain period of time, the task requesting the resource resumes execution. μC/OS-III returns an error code indicating that a timeout occurred. A return error code prevents the task from thinking it has properly obtained the resource. 247 Chapter 13 The pseudo-code avoids deadlocks by first acquiring all resources as shown in Listing 13-17. void T1 (void *p_arg) { while (DEF_ON) { Wait for event to occur; Acquire M1; Acquire M2; Access R1; Access R2; } } void T2 (void *p_arg) { while (DEF_ON) { Wait for event to occur; Acquire M1; Acquire M2; Access R1; Access R2; } } Listing 13-17 Deadlock avoidance – acquire all first and in the same order 248 Resource Management The pseudo-code to acquire all of the mutexes in the same order is shown in Listing 13-18. This is similar to the previous example, except that it is not necessary to acquire all the mutexes first, only to make sure that the mutexes are acquired in the same order for both tasks. void T1 (void *p_arg){ while (DEF_ON) { Wait for event to occur; Acquire Access Acquire Access M1; R1; M2; R2; } } void T2 (void *p_arg) { while (DEF_ON) { Wait for event to occur; Acquire M1; Access R1; Acquire M2; Access R2; } } Listing 13-18 Deadlock avoidance – acquire in the same order 249 Chapter 13 13-7 SUMMARY The mutual exclusion mechanism used depends on how fast code will access the shared resource, as shown in Table 13-4. Resource Sharing Method When should you use? Disable/Enable Interrupts When access to shared resource is very quick (reading from or writing to just a few variables) and the access is actually faster than μC/OS-III’s interrupt disable time. It is highly recommended to not use this method as it impacts interrupt latency. Locking/Unlocking the Scheduler When access time to the shared resource is longer than μC/OS-III’s interrupt disable time, but shorter than μC/OS-III’s scheduler lock time. Locking the scheduler has the same effect as making the task that locks the scheduler the highest priority task. It is recommended to not use this method since it defeats the purpose of using μC/OS-III. However, it’s a better method than disabling interrupts as it does not impact interrupt latency. Semaphores When all tasks that need to access a shared resource do not have deadlines. This is because semaphores can cause unbounded priority inversions. However, semaphore services are slightly faster (in execution time) than mutual exclusion semaphores. Mutual Exclusion Semaphores This is the preferred method for accessing shared resources, especially if the tasks that need to access a shared resource have deadlines. Remember that mutual exclusion semaphores have a built-in priority inheritance mechanism, which avoids unbounded priority inversions. However, mutual exclusion semaphore services are slightly slower (in execution time) than semaphores, because the priority of the owner may need to be changed, which requires CPU processing. Table 13-4 Resource sharing summary 250 Chapter 14 Synchronization This chapter focuses on how tasks can synchronize their activities with Interrupt Service Routines (ISRs), or other tasks. When an ISR executes, it can signal a task telling the task that an event of interest has occurred. After signaling the task, the ISR exits and, depending on the signaled task priority, the scheduler is run. The signaled task may then service the interrupting device, or otherwise react to the event. Serving interrupting devices from task level is preferred whenever possible, since it reduces the amount of time that interrupts are disabled and the code is easier to debug. There are two basic mechanisms for synchronizations in μC/OS-III: semaphores and event flags. 251 Chapter 14 14-1 SEMAPHORES As defined in Chapter 13, “Resource Management” on page 209, a semaphore is a protocol mechanism offered by most multitasking kernels. Semaphores were originally used to control access to shared resources. However, better mechanisms exist to protect access to shared resources, as described in Chapter 12. Semaphores are best used to synchronize an ISR to a task, or synchronize a task with another task as shown in Figure 14-1. Note that the semaphore is drawn as a flag to indicate that it is used to signal the occurrence of an event. The initial value for the semaphore is typically zero (0), indicating the event has not yet occurred. The value “N” next to the flag indicates that the semaphore can accumulate events or credits. It is possible to initialize the semaphore with a value other than zero, indicating that the semaphore initially contains that number of events. An ISR (or a task) can post (or signal) multiple times to a semaphore and the semaphore will remember how many times it was posted. Also, the small hourglass close to the receiving task indicates that the task has an option to specify a timeout. This timeout indicates that the task is willing to wait for the semaphore to be signaled (or posted to) within a certain amount of time. If the semaphore is not signaled within that time, μC/OS-III resumes the task and returns an error code indicating that the task was made ready to run because of a timeout and not the semaphore was signaled. 7DVN 266HP&UHDWH 266HP'HO 266HP3HQG$ERUW 266HP3RVW 266HP6HW 266HP3RVW ,65 266HP3HQG 1 7DVN 7LPHRXW Figure 14-1 μC/OS-III Semaphore Services There are a number of operations to perform on semaphores as summarized in Table 14-1 and Figure 14-1. However, in this chapter, we will only discuss the three functions used most often: OSSemCreate(), OSSemPend(), and OSSemPost(). The other functions are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. Also note that every semaphore function is callable from a task, but only OSSemPost() can be called by an ISR 252 Synchronization . Function Name Operation OSSemCreate() Create a semaphore. OSSemDel() Delete a semaphore. OSSemPend() Wait on a semaphore. OSSemPendAbort() Abort the wait on a semaphore. OSSemPost() Signal a semaphore. OSSemSet() Force the semaphore count to a desired value. Table 14-1 Semaphore API summary When used for synchronization, a semaphore keeps track of how many times it was signaled using a counter. The counter can take values between 0 and 255, 65,535, or 4,294,967,295, depending on whether the semaphore mechanism is implemented using 8, 16, or 32 bits, respectively. For μC/OS-III, the maximum value of a semaphore is determined by the data type OS_SEM_CTR (see OS_TYPE.H), which is changeable, as needed (assuming access to μC/OS-III’s source code). Along with the semaphore’s value, μC/OS-III keeps track of tasks waiting for the semaphore to be signaled. 253 Chapter 14 14-1-1 UNILATERAL RENDEZVOUS Figure 14-2 shows that a task can be synchronized with an ISR (or another task) by using a semaphore. In this case, no data is exchanged, however there is an indication that the ISR or the task (on the left) has occurred. Using a semaphore for this type of synchronization is called a unilateral rendezvous. 7DVN 266HP3HQG 266HP3RVW 1 ,65 7DVN 7LPHRXW 266HP3HQG 266HP3RVW 1 7DVN 7LPHRXW Figure 14-2 Unilateral Rendezvous A unilateral rendezvous is used when a task initiates an I/O operation and waits (i.e., call OSSemPend()) for the semaphore to be signaled (posted). When the I/O operation is complete, an ISR (or another task) signals the semaphore (i.e., calls OSSemPost()), and the task is resumed. This process is also shown on the timeline of Figure 14-3 and described below. The code for the ISR and task is shown in Listing 14-1.  ,65 266HP3RVW   266HP3HQG +3 7DVN /3 7DVN         Figure 14-3 Unilateral Rendezvous, Timing Diagram 254 Synchronization F14-3(1) F14-3(2) F14-3(3) F14-3(4) A high priority task is executing. The task needs to synchronize with an ISR (i.e., wait for the ISR to occur) and call OSSemPend(). Since the ISR has not occurred, the task will be placed in the waiting list for the semaphore until the event occurs The scheduler in μC/OS-III will then select the next most important task and context switch to that task. F14-3(5) The low-priority task executes. F14-3(6) The event that the original task was waiting for occurs. The lower-priority task is immediately preempted (assuming interrupts are enabled), and the CPU vectors to the interrupt handler for the event. F14-3(7) F14-3(8) F14-3(9) F14-3(10) F14-3(11) The ISR handles the interrupting device and then calls OSSemPost() to signal the semaphore. When the ISR completes, μC/OS-III is called. μC/OS-III notices that a higher-priority task is waiting for this event to occur and context switches back to the original task. The original task resumes execution immediately after the call to OSSemPend(). 255 Chapter 14 OS_SEM MySem; void MyISR (void) { OS_ERR err; /* Clear the interrupting device */ OSSemPost(&MySem, OS_OPT_POST_1, &err); /* Check “err” */ (7) } void MyTask (void *p_arg) { OS_ERR err; CPU_TS ts; : : while (DEF_ON) { OSSemPend(&MySem, 10, OS_OPT_PEND_BLOCKING, &ts, &err); /* Check “err” */ : : } } (1) (11) Listing 14-1 Pending (or waiting) on a Semaphore A few interesting things are worth noting about this process. First, the task does not need to know about the details of what happens behind the scenes. As far as the task is concerned, it called a function (OSSemPend()) that will return when the event it is waiting for occurs. Second, μC/OS-III maximizes the use of the CPU by selecting the next most important task, which executes until the ISR occurs. In fact, the ISR may not occur for many milliseconds and, during that time, the CPU will work on other tasks. As far as the task that is waiting for the semaphore is concerned, it does not consume CPU time while it is waiting. Finally, the task waiting for the semaphore will execute immediately after the event occurs (assuming it is the most important task that needs to run). 256 Synchronization 14-1-2 CREDIT TRACKING As previously mentioned, a semaphore “remembers” how many times it was signaled (or posted to). In other words, if the ISR occurs multiple times before the task waiting for the event becomes the highest-priority task, the semaphore will keep count of the number of times it was signaled. When the task becomes the highest priority ready-to-run task, it will execute without blocking as many times as there were ISRs signaled. This is called Credit Tracking and is illustrated in Figure 14-4 and described below. 6HP  ,65  6HP  266HP3RVW 266HP3RVW   —&26,,,     7DVN       7DVN     266HP3HQG Figure 14-4 Semaphore Credit Tracking F14-4(1) F14-4(2) F14-4(3) F14-4(4) F14-4(5) F14-4(6) A high-priority task is executing. An event meant for a lower-priority task occurs which preempts the task (assuming interrupts are enabled). The ISR executes and posts the semaphore. At this point the semaphore count is 1. μC/OS-III is called at the end of the ISR to see if the ISR caused a higher-priority task to be ready to run. Since the ISR was an event that a lower-priority task was waiting on, μC/OS-III will resume execution of the higher-priority task at the exact point where it was interrupted. 257 Chapter 14 F14-4(7) F14-4(8) F14-4(9) F14-4(10) F14-4(11) F14-4(12) F14-4(13) F14-4(14) F14-4(15) F14-4(16) F14-4(17) 258 The high-priority task is resumed and continues execution. The interrupt occurs a second time. The ISR executes and posts the semaphore. At this point the semaphore count is 2. μC/OS-III is called at the end of the ISR to see if the ISR caused a higher-priority task to be ready to run. Since the ISR was an event that a lower-priority task was waiting on, μC/OS-III resumes execution of the higher-priority task at the exact point where it was interrupted. The high-priority task resumes execution and actually terminates the work it was doing. This task will then call one of the μC/OS-III services to wait for “its” event to occur. μC/OS-III will then select the next most important task, which happens to be the task waiting for the event and will context switch to that task. The new task executes and will know that the ISR occurred twice since the semaphore count is two. The task will handle this accordingly. Synchronization 14-1-3 MULTIPLE TASKS WAITING ON A SEMAPHORE It is possible for more than one task to wait on the same semaphore, each with its own timeout as illustrated in Figure 14-5. 7DVN 7LPHRXW 7DVN 266HP3HQG 266HP3RVW 266HP3HQG 266HP3RVW ,65 1 7DVN 7LPHRXW 266HP3HQG 7LPHRXW 7DVN Figure 14-5 Multiple Tasks waiting on a Semaphore When the semaphore is signaled (whether by an ISR or task), μC/OS-III makes the highest-priority task waiting on the semaphore ready to run. However, it is also possible to specify that all tasks waiting on the semaphore be made ready to run. This is called broadcasting and is accomplished by specifying OS_OPT_POST_ALL as an option when calling OSSemPost(). If any of the waiting tasks has a higher priority than the previously running task, μC/OS-III will execute the highest-priority task made ready by OSSemPost(). Broadcasting is a common technique used to synchronize multiple tasks and have them start executing at the same time. However, some of the tasks that we want to synchronize might not be waiting for the semaphore. It is fairly easy to resolve this problem by combining semaphores and event flags. This will be described after examining event flags. 259 Chapter 14 14-1-4 SEMAPHORE INTERNALS (FOR SYNCHRONIZATION) Note that some of the material presented in this section is also contained in Chapter 13, “Resource Management” on page 209, as semaphores were also discussed in that chapter. However, the material presented here will be applicable to semaphores used for synchronization and thus will differ somewhat. A counting semaphore allows values between 0 and 255, 65,535, or 4,294,967,295, depending on whether the semaphore mechanism is implemented using 8, 16, or 32 bits, respectively. For μC/OS-III, the maximum value of a semaphore is determined by the data type OS_SEM_CTR (see OS_TYPE.H), which can be changed as needed (assuming access to μC/OS-III’s source code). Along with the semaphore’s value, μC/OS-III keeps track of tasks waiting for the semaphore’s availability. The application programmer can create an unlimited number of semaphores (limited only by available RAM). Semaphore services in μC/OS-III start with the OSSem???() prefix, and services available to the application programmer are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. Semaphore services are enabled at compile time by setting the configuration constant OS_CFG_SEM_EN to 1 in OS_CFG.H. Semaphores must be created before they can be used by the application. Listing 14-3 shows how to create a semaphore. As previously mentioned, a semaphore is a kernel object as defined by the OS_SEM data type, which is derived from the structure os_sem (see OS.H) as shown in Listing 14-2. The services provided by μC/OS-III to manage semaphores are implemented in the file OS_SEM.C. μC/OS-III licensees, have access to the source code. 260 Synchronization typedef struct struct os_sem { OS_OBJ_TYPE CPU_CHAR OS_PEND_LIST OS_SEM_CTR CPU_TS }; os_sem OS_SEM; (1) Type; *NamePtr; PendList; Ctr; TS; (2) (3) (4) (5) (6) Listing 14-2 OS_SEM data type L14-2(1) In μC/OS-III, all structures are given a data type. In fact, all data types start with “OS_” and are all uppercase. When a semaphore is declared, simply use OS_SEM as the data type of the variable used to declare the semaphore. L14-2(2) The structure starts with a “Type” field, which allows it to be recognized by μC/OS-III as a semaphore. In other words, other kernel objects will also have a “Type” as the first member of the structure. If a function is passed a kernel object, μC/OS-III will confirm that it is being passed the proper data type. For example, if passing a message queue (OS_Q) to a semaphore service (for example OSSemPend()), μC/OS-III will recognize that an invalid object was passed, and return an error code accordingly. L14-2(3) Each kernel object can be given a name to make them easier to be recognized by debuggers or μC/Probe. This member is simply a pointer to an ASCII string, which is assumed to be NUL terminated. L14-2(4) Since it is possible for multiple tasks to be waiting (or pending) on a semaphore, the semaphore object contains a pend list as described in Chapter 10, “Pend Lists (or Wait Lists)” on page 177. L14-2(5) A semaphore contains a counter. As explained above, the counter can be implemented as either an 8-, 16- or 32-bit value, depending on how the data type OS_SEM_CTR is declared in OS_TYPE.H. μC/OS-III keeps track of how many times the semaphore is signaled with this counter and this field is typically initialized to zero by OSSemCreate(). 261 Chapter 14 L14-2(6) A semaphore contains a time stamp, which is used to indicate the last time the semaphore was signaled (or posted to). μC/OS-III assumes the presence of a free-running counter that allows the application to make time measurements. When the semaphore is signaled, the free-running counter is read and the value is placed in this field, which is returned when OSSemPend() is called. This value allows the application to determine either when the signal was performed, or how long it took for the task to get control of the CPU from the signal. In the latter case, call OS_TS_GET() to determine the current timestamp and compute the difference. Even for users who understand the internals of the OS_SEM data type, the application code should never access any of the fields in this data structure directly. Instead, always use the APIs provided with μC/OS-III. Semaphores must be created before they can be used by an application. Listing 14-3 shows how to create a semaphore. OS_SEM void { MySem; (1) MyCode (void) OS_ERR err; : OSSemCreate(&MySem, “My Semaphore”, (OS_SEM_CTR)0, &err); /* Check “err” */ : (2) (3) (4) (5) } Listing 14-3 Creating a Semaphore L14-3(1) The application must declare a variable of type OS_SEM. This variable will be referenced by other semaphore services. L14-3(2) Create a semaphore by calling OSSemCreate() and pass the address to the semaphore allocated in (1). 262 Synchronization L14-3(3) Assign an ASCII name to the semaphore, which can be used by debuggers or μC/Probe to easily identify this semaphore. L14-3(4) Initialize the semaphore to zero (0) when using a semaphore as a signaling mechanism. L14-3(5) OSSemCreate() returns an error code based on the outcome of the call. If all arguments are valid, err will contain OS_ERR_NONE. OSSemCreate() performs a check on the arguments passed to this function and only initializes the contents of the variable of type OS_SEM used for signaling. A task waits for a signal from an ISR or another task by calling OSSemPend() as shown in Listing 14-4 (see Appendix A, “μC/OS-III API Reference Manual” on page 375 for details regarding the arguments). OS_SEM MySem; void MyCode (void) { OS_ERR err; : OSSemCreate(&MySem, “My Semaphore”, (OS_SEM_CTR)0, &err); /* Check “err” */ : } (1) (2) (3) (4) (5) Listing 14-4 Pending (or waiting) on a Semaphore L14-4(1) When called, OSSemPend() starts by checking the arguments passed to this function to make sure they have valid values. If the semaphore counter (.Ctr of OS_SEM) is greater than zero, the counter is decremented and OSSemPend() returns, which indicates that the signal occurred. This is the outcome that the caller expects. 263 Chapter 14 If the semaphore counter is zero, this indicates that the signal has not occurred and the calling task might need to wait for the semaphore to be released. If specifying OS_OPT_PEND_NON_BLOCKING as the option (the task is not to block), OSSemPend() returns immediately to the caller and the returned error code will indicate that the signal did not occur. If specifying OS_OPT_PEND_BLOCKING as the option, the calling task will be inserted in the list of tasks waiting for the semaphore to be signaled. The task is inserted in the list by priority order with the highest priority task waiting on the semaphore at the beginning of the list as shown in Figure 14-6. If further specifying a non-zero timeout, the task will also be inserted in the tick list. A zero value for a timeout indicates that the calling task is willing to wait forever for the semaphore to be signaled. The scheduler is then called as the current task is not able to run (it is waiting for the semaphore to be signaled). The scheduler will then run the next highest-priority task that is ready to run. When the semaphore is signaled and the task that called OSSemPend() is again the highest-priority task, a task status is examined to determine the reason why OSSemPend() is returning to its caller. The possibilities are: 1) The semaphore was signaled 2) The pend was aborted by another task 3) The semaphore was not signaled within the specified timeout 4) The semaphore was deleted When OSSemPend() returns, the caller is notified of the above outcome through an appropriate error code. L14-4(2) 264 If OSSemPend() returns with err set to OS_ERR_NONE, assume that the semaphore was signaled and the task can proceed with servicing the ISR or task that caused the signal. If err contains anything else, OSSemPend() either timed out (if the timeout argument was non-zero), the pend was aborted by Synchronization another task, or the semaphore was deleted by another task. It is always important to examine returned error code and not assume everything went as expected. To signal a task (either from an ISR or a task), simply call OSSemPost() as shown in Listing 14-5. OS_SEM MySem; void MyISR (void) { OS_ERR err; : OSSemPost(&MySem, OS_OPT_POST_1, &err); /* Check “err” */ : : } (1) (2) (3) Listing 14-5 Posting (or signaling) a Semaphore L14-5(1) Your task signals (or posts to) the semaphore by calling OSSemPost(). Specify the semaphore to post by passing its address. The semaphore must have been previously created. L14-5(2) The next argument specifies how the task wants to post. There are a number of options to choose from. Specify OS_OPT_POST_1, which indicates posting to only one task in case there are multiple tasks waiting on the semaphore. The task that will be made ready to run will be the highest-priority task waiting on the semaphore. If there are multiple tasks at the same priority, only one of them will be made ready-to-run. As shown in Figure 14-6, tasks waiting are in priority order (HPT means High Priority Task and LPT means Low Priority Task). It is a fast operation to extract the HPT from the list. 265 Chapter 14 If specifying OS_OPT_POST_ALL, all tasks waiting on the semaphore will be posted and made ready to run. The calling task can “add” the option OS_OPT_POST_NO_SCHED to either of the two previous options to indicate that the scheduler is not to be called at the end of OSSemPost(), possibly because additional postings will be performed, and rescheduling should take place when finished. This means that the signal is performed, but the scheduler is not called even if a higher-priority task was waiting for the semaphore to be signaled. This allows the calling task to perform other post functions (if needed) and make all the posts take effect simultaneously. Note that OS_OPT_POST_NO_SCHED is “additive,” meaning that it can be used with either of the previous options. You can specify: OS_OPT_POST_1 OS_OPT_POST_ALL OS_OPT_POST_1 + OS_OPT_POST_NO_SCHED OS_OPT_POST_ALL + OS_OPT_POST_NO_SCHED 26B6(0 7\SH 7DVNV :DLWLQJ IRU 6HPDSKRUH 1DPH3WU &WU 1EU(QWULHV +HDG3WU 7DLO3WU  3UHY3WU 1H[W3WU +37 3UHY3WU 1H[W3WU 3UHY3WU 1H[W3WU  /37 Figure 14-6 Tasks waiting for semaphore 266 Synchronization L14-5(3) OSSemPost() returns an error code based on the outcome of the call. If the call was successful, err will contain OS_ERR_NONE. If not, the error code will indicate the reason for the error (see Appendix A, “μC/OS-III API Reference Manual” on page 375 for a list of possible error codes for OSSemPost(). 14-2 TASK SEMAPHORE Signaling a task using a semaphore is a very popular method of synchronization and, in μC/OS-III, each task has its own built-in semaphore. This feature not only simplifies code, but is also more efficient than using a separate semaphore object. The semaphore, which is built into each task, is shown in Figure 14-7. Task semaphore services in μC/OS-III start with the OSTaskSem???() prefix, and the services available to the application programmer are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. Task semaphores are built into μC/OS-III and cannot be disabled at compile time as can other services. The code for task semaphores is found in OS_TASK.C. Use this feature if the code knows which task to signal when the event occurs. For example, if receiving an interrupt from an Ethernet controller, signal the task responsible for processing the received packet as it is preferable to perform this processing using a task instead of the ISR. 7DVN 267DVN6HP3HQG$ERUW 267DVN6HP3RVW 267DVN6HP6HW 267DVN6HP3HQG 267DVN6HP3RVW ,65 1 7DVN 7LPHRXW Figure 14-7 Semaphore built-into a Task There are a number of operations to perform on task semaphores, summarized in Table 14-2. 267 Chapter 14 Function Name Operation OSTaskSemPend() Wait on a task semaphore. OSTaskSemPendAbort() Abort the wait on a task semaphore. OSTaskSemPost() Signal a task. OSTaskSemSet() Force the semaphore count to a desired value. Table 14-2 Task Semaphore API summary 14-2-1 PENDING (i.e., WAITING) ON A TASK SEMAPHORE When a task is created, it automatically creates an internal semaphore with an initial value of zero (0). Waiting on a task semaphore is quite simple, as shown in Listing 14-6. void MyTask (void *p_arg) { OS_ERR err; CPU_TS ts; : while (DEF_ON) { OSTaskSemPend(10, OS_OPT_PEND_BLOCKING, &ts, &err); /* Check “err” */ : : } (1) (2) (3) (4) Listing 14-6 Pending (or waiting) on Task’s internal semaphore L14-6(1) 268 A task pends (or waits) on the task semaphore by calling OSTaskSemPend(). There is no need to specify which task, as the current task is assumed. The first argument is a timeout specified in number of clock ticks. The actual timeout obviously depends on the tick rate. If the tick rate (see OS_CFG_APP.H) is set to 1000, a timeout of 10 ticks represents 10 milliseconds. Specifying a timeout of zero (0) means that the task will wait forever for the task semaphore. Synchronization L14-6(2) The second argument specifies how to pend. There are OS_OPT_PEND_BLOCKING and OS_OPT_PEND_NON_BLOCKING. option means that, if the task semaphore has not been signaled the task will wait until the semaphore is signaled, the pend another task or, until the timeout expires. two options: The blocking (or posted to), is aborted by L14-6(3) When the semaphore is signaled, μC/OS-III reads a “timestamp” and places it in the receiving task’s OS_TCB. When OSTaskSemPend() returns, the value of the timestamp is placed in the local variable “ts”. This feature captures “when” the signal actually happened. Call OS_TS_GET() to read the current timestamp and compute the difference. This establishes how long it took for the task to receive the signal from the posting task or ISR. L14-6(4) OSTaskSemPend() returns an error code based on the outcome of the call. If the call was successful, err will contain OS_ERR_NONE. If not, the error code will indicate the reason of the error (see Appendix A, “μC/OS-III API Reference Manual” on page 375 for a list of possible error code for OSTaskSemPend(). 14-2-2 POSTING (i.e., SIGNALING) A TASK SEMAPHORE An ISR or a task signals a task by calling OSTaskSemPost(), as shown in Listing 14-7. OS_TCB MyTaskTCB; void MyISR (void *p_arg) { OS_ERR err; : OSTaskSemPost(&MyTaskTCB, OS_OPT_POST_NONE, &err); /* Check “err” */ : : } (1) (2) (3) Listing 14-7 Posting (or signaling) a Semaphore 269 Chapter 14 L14-7(1) A task posts (or signals) the task by calling OSTaskSemPost(). It is necessary to pass the address of the desired task’s OS_TCB. Of course, the task must exist. L14-7(2) The next argument specifies how the user wants to post. There are only two choices. Specify OS_OPT_POST_NONE, which indicates the use of the default option of calling the scheduler after posting the semaphore. Or, specify OS_OPT_POST_NO_SCHED to indicate that the scheduler is not to be called at the end of OSTaskSemPost(), possibly because there will be additional postings, and rescheduling would take place when finished (the last post would not specify this option). L14-7(3) 270 OSTaskSemPost() returns an error code based on the outcome of the call. If the call was successful, err will contain OS_ERR_NONE. If not, the error code will indicate the reason of the error (see Appendix A, “μC/OS-III API Reference Manual” on page 375 for a list of possible error codes for OSTaskSemPost(). Synchronization 14-2-3 BILATERAL RENDEZVOUS Two tasks can synchronize their activities by using two task semaphores, as shown in Figure 14-8, and is called a bilateral rendezvous. A bilateral rendezvous is similar to a unilateral rendezvous, except that both tasks must synchronize with one another before proceeding. A bilateral rendezvous cannot be performed between a task and an ISR because an ISR cannot wait on a semaphore. 7DVN  267DVN6HP3HQG 267DVN6HP3RVW 267DVN6HP3RVW 267DVN6HP3RVW 7DVN  Figure 14-8 Bilateral Rendezvous The code for a bilateral rendezvous is shown in Listing 14-8. Of course, a bilateral rendezvous can use two separate semaphores, but the built-in task semaphore makes setting up this type of synchronization quite straightforward. 271 Chapter 14 OS_TCB MyTask1_TCB; OS_TCB MyTask2_TCB; void Task1 (void *p_arg) { OS_ERR err; CPU_TS ts; while (DEF_ON) { : OSTaskSemPost(&MyTask2_TCB, OS_OPT_POST_NONE, &err); /* Check ’err” */ OSTaskSemPend(0, OS_OPT_PEND_BLOCKING, &ts, &err); /* Check ’err” */ : } (1) (2) } void Task2 (void *p_arg) { OS_ERR err; CPU_TS ts; while (DEF_ON) { : OSTaskSemPost(&MyTask1_TCB, OS_OPT_POST_NONE, &err); /* Check ’err” */ OSTaskSemPend(0, OS_OPT_PEND_BLOCKING, &ts, &err); /* Check ’err” */ : } (3) (4) } Listing 14-8 Tasks synchronizing their activities 272 Synchronization L14-8(1) Task #1 is executing and signals Task #2’s semaphore. L14-8(2) Task #1 pends on its internal semaphore to synchronize with Task #2. Because Task #2 has not executed yet, Task #1 is blocked waiting on its semaphore to be signaled. μC/OS-III context switches to Task #2. L14-8(3) Task #2 executes, and signals Task #1’s semaphore. L14-8(4) Since it has already been signaled, Task #2 is now synchronized to Task #1. If Task #1 is higher in priority than Task #2, μC/OS-III will switch back to Task #1. If not, Task #2 continues execution. 14-3 EVENT FLAGS Event flags are used when a task needs to synchronize with the occurrence of multiple events. The task can be synchronized when any of the events have occurred, which is called disjunctive synchronization (logical OR). A task can also be synchronized when all events have occurred, which is called conjunctive synchronization (logical AND). Disjunctive and conjunctive synchronization are shown in Figure 14-9. The application programmer can create an unlimited number of event flag groups (limited only by available RAM). Event flag services in μC/OS-III start with the OSFlag???() prefix. The services available to the application programmer are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. The code for event flag services is found in the file OS_FLAG.C, and is enabled at compile time by setting the configuration constant OS_CFG_FLAG_EN to 1 in OS_CFG.H. 273 Chapter 14  (YHQW )ODJ *URXS 26B)/$*B*53 7DVN 26)ODJ&UHDWH 26)ODJ'HO 26)ODJ3HQG$ERUW 26)ODJ3RVW  26)ODJ3RVW ,65 } } 26)ODJ3HQG*HW)ODJV5G\ $1' 26)ODJ3HQG 7DVN 7LPHRXW   26)ODJ3HQG*HW)ODJV5G\ 25 26)ODJ3HQG 7DVN 7LPHRXW Figure 14-9 Event Flags F14-9(1) A μC/OS-III “event flag group” is a kernel object of type OS_FLAG_GRP (see OS.H), and consists of a series of bits (8-, 16- or 32-bits, based on the data type OS_FLAGS defined in OS_TYPE.H). The event flag group also contains a list of tasks waiting for some (or all) of the bits to be set (1) or clear (0). An event flag group must be created before it can be used by tasks and ISRs. Create event flags prior to starting μC/OS-III, or by a startup task in the application code. F14-9(2) Tasks or ISRs can post to event flags. In addition, only tasks can create, delete, and stop other task from pending on event flag groups. F14-9(3) A task can wait (i.e., pend) on any number of bits in an event flag group (i.e., a subset of all the bits). As with all μC/OS-III pend calls, the calling task can specify a timeout value such that if the desired bits are not posted within a specified amount of time (in ticks), the pending task is resumed and informed about the timeout. F14-9(4) The task can specify whether it wants to wait for “any” subset of bits (OR) to be set (or clear), or wait for “all” bits in a subset of bit (AND) to be set (or clear). 274 Synchronization There are a number of operations to perform on event flags, as summarized in Table 14-3. Function Name Operation OSFlagCreate() Create an event flag group OSFlagDel() Delete an event flag group OSFlagPend() Pend (i.e., wait) on an event flag group OSFlagPendAbort() Abort waiting on an event flag group OSFlagPendGetFlagsRdy() Get flags that caused task to become ready OSFlagPost() Post flag(s) to an event flag group Table 14-3 Event Flags API summary 14-3-1 USING EVENT FLAGS When a task or an ISR posts to an event flag group, all tasks that have their wait conditions satisfied will be resumed. It’s up to the application to determine what each bit in an event flag group means and it is possible to use as many event flag groups as needed. In an event flag group you can, for example, define that bit #0 indicates that a temperature sensor is too low, bit #1 may indicate a low battery voltage, bit #2 could indicate that a switch was pressed, etc. The code (tasks or ISRs) that detects these conditions would set the appropriate event flag by calling OSFlagPost() and the task(s) that would respond to those conditions would call OSFlagPend(). Listing 14-9 shows how to use event flags. 275 Chapter 14 #define TEMP_LOW #define #define BATT_LOW (OS_FLAGS)0x0002 SW_PRESSED (OS_FLAGS)0x0004 (OS_FLAGS)0x0001 OS_FLAG_GRP MyEventFlagGrp; (1) (2) void main (void) { OS_ERR err; OSInit(&err); : OSFlagCreate(&MyEventFlagGrp, ”My Event Flag Group”, (OS_FLAGS)0, &err); (3) /* Check ’err” */ : OSStart(&err); } void MyTask (void *p_arg) { OS_ERR err; CPU_TS ts; while (DEF_ON) { OSFlagPend(&MyEventFlagGrp, TEMP_LOW + BATT_LOW, (OS_TICK )0, (OS_OPT)OS_OPT_PEND_FLAG_SET_ANY, &ts, &err); /* Check ’err” */ : } } 276 (4) (5) Synchronization void { MyISR (void) OS_ERR err; : OSFlagPost(&MyEventFlagGrp, (6) (7) BAT_LOW, (OS_OPT)OS_OPT_POST_FLAG_SET, &err); /* Check ’err” */ : } Listing 14-9 Using Event Flags L14-9(1) Define some bits in the event flag group. L14-9(2) Declare an object of type OS_FLAG_GRP. This object will be referenced in all subsequent μC/OS-III calls that apply to this event flag group. For the sake of discussions, assume that event flags are declared to be 16-bits in OS_TYPE.H (i.e., of type CPU_INT16U). L14-9(3) Event flag groups must be “created” before they can be used. The best place to do this is in your startup code as it ensures that no tasks, or ISR, will be able to use the event flag group until μC/OS-III is started. In other words, the best place is to create the event flag group is in main(). In the example, the event flag was given a name and all bits start in their cleared state (i.e., all zeros). L14-9(4) Assume that the application created “MyTask()” which will be pending on the event flag group. L14-9(5) To pend on an event flag group, call OSFlagPend() and pass it the address of the desired event flag group. The second argument specifies which bits the task will be waiting to be set (assuming the task is triggered by set bits instead of cleared bits). Specify how long to wait for these bits to be set. A timeout value of zero (0) indicates that the task will wait forever. A non-zero value indicates the number of ticks the task will wait until it is resumed if the desired bits are not set. 277 Chapter 14 Specifying OS_OPT_FLAG_SET_ANY indicates that the task will wake up if either of the two bits specified is set. A timestamp is read and saved when the event flag group is posted to. This timestamp can be used to determine the response time to the event. OSFlagPend() performs a number of checks on the arguments passed (i.e., did you pass NULL pointers, invalid options, etc.), and returns an error code based on the outcome of the call. If the call was successful “err” will be set to OS_ERR_NONE. L14-9(6) An ISR (it can also be a task) is setup to detect when the battery voltage of the product goes low (assuming the product is battery operated). The ISR signals the task, letting the task perform whatever corrective action is needed. L14-9(7) The desired event flag group is specified in the post call as well as which flag the ISR is setting. The third option specifies that the error condition will be “flagged” as a set bit. Again, the function sets “err” based on the outcome of the call. Event flags are generally used for two purposes: status and transient events. Typically use different event flag groups to handle each of these as shown in Listing 14-10. Tasks or ISRs can report status information such as a temperature that has exceeded a certain value, that RPM is zero on an engine or motor, or there is fuel in the tank, and more. This status information cannot be “consumed” by the tasks waiting for these events, because the status is managed by other tasks or ISRs. Event flags associated with status information are monitored by other task by using non-blocking wait calls. Tasks will report transient events such as a switch was pressed, an object was detected by a motion sensor, an explosion occurred, etc. The task that responds to these events will typically block waiting for any of those events to occur and “consume” the event. 278 Synchronization 6WDWXV 26B)/$*B*53 530 7DVN 530  530! } $1' 26)ODJ3HQG 121B%/2&.,1* 'LVSOD\ 7DVN } 25 26)ODJ3HQG 121B%/2&.,1* 'DWD /RJJLQJ 7DVN $LU7HPS ! $QDORJ ,QSXW 7DVN $LU3UHV ! )XHO/HYHO! 530! 6XGGHQ 6WRS 0RWLRQ 6HQVRU 7DVN ([SORVLRQ ,65 6ZLWFK 7DVN 0RWLRQ'HWHFWHG ([SORVLRQ } 25 } 25 26)ODJ3HQG %/2&.,1*&21680( (UURU 7DVN 7LPHRXW 26)ODJ3HQG %/2&.,1*&21680( $ERUW 7DVN 7LPHRXW $ERUW 7UDQVLHQW(YHQWV 26B)/$*B*53 Figure 14-10 Event Flags used for Status and Transient Events 14-3-2 EVENT FLAGS INTERNALS The application programmer can create an unlimited number of event flag groups (limited only by available RAM). Event flag services in μC/OS-III start with OSFlag and the services available to the application programmer are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. Event flag services are enabled at compile time by setting the configuration constant OS_CFG_FLAG_EN to 1 in OS_CFG.H. An event flag group is a kernel object as defined by the OS_FLAG_GRP data type, which is derived from the structure os_flag_grp (see OS.H) as shown in Listing 14-10. 279 Chapter 14 The services provided by μC/OS-III to manage event flags are implemented in the file OS_FLAG.C. μC/OS-III licensees have access to the source code. typedef struct os_flag_grp struct os_flag_grp { OS_OBJ_TYPE CPU_CHAR OS_PEND_LIST OS_FLAGS CPU_TS }; OS_FLAG_GRP; Type; *NamePtr; PendList; Flags; TS; (1) (2) (3) (4) (5) (6) Listing 14-10 OS_FLAG_GRP data type L14-10(1) In μC/OS-III, all structures are given a data type. In fact, all data types start with “OS_” and are uppercase. When an event flag group is declared, simply use OS_FLAG_GRP as the data type of the variable used to declare the event flag group. L14-10(2) The structure starts with a “Type” field, which allows it to be recognized by μC/OS-III as an event flag group. In other words, other kernel objects will also have a “Type” as the first member of the structure. If a function is passed a kernel object, μC/OS-III will be able to confirm that it is being passed the proper data type. For example, if passing a message queue (OS_Q) to an event flag service (for example OSFlagPend()), μC/OS-III will be able to recognize that an invalid object was passed, and return an error code accordingly. L14-10(3) Each kernel object can be given a name to make them easier to be recognized by debuggers or μC/Probe. This member is simply a pointer to an ASCII string, which is assumed to be NUL terminated. L14-10(4) Because it is possible for multiple tasks to be waiting (or pending) on an event flag group, the event flag group object contains a pend list as described in Chapter 10, “Pend Lists (or Wait Lists)” on page 177. 280 Synchronization L14-10(5) An event flag group contains a series of flags (i.e., bits), and this member contains the current state of these flags. The flags can be implemented using either an 8-, 16- or 32-bit value depending on how the data type OS_FLAGS is declared in OS_TYPE.H. L14-10(6) An event flag group contains a timestamp used to indicate the last time the event flag group was posted to. μC/OS-III assumes the presence of a free-running counter that allows users to make time measurements. When the event flag group is posted to, the free-running counter is read and the value is placed in this field, which is returned when OSFlagPend() is called. This value allows an application to determine either when the post was performed, or how long it took for your the to obtain control of the CPU from the post. In the latter case, call OS_TS_GET() to determine the current timestamp and compute the difference. Even if the user understands the internals of the OS_FLAG_GRP data type, application code should never access any of the fields in this data structure directly. Instead, always use the APIs provided with μC/OS-III. Event flag groups must be created before they can be used by an application as shown in Listing 14-11. OS_FLAG_GRP MyEventFlagGrp; void MyCode (void) { OS_ERR err; : OSFlagCreate(&MyEventFlagGrp, “My Event Flag Group”, (OS_FLAGS)0, &err); /* Check ’err” */ : } (1) (2) (3) (4) (5) Listing 14-11 Creating a Event Flag Group 281 Chapter 14 L14-11(1) The application must declare a variable of type OS_FLAG_GRP. This variable will be referenced by other event flag services. L14-11(2) Create an event flag group by calling OSFlagCreate() and pass the address to the event flag group allocated in (1). L14-11(3) Assign an ASCII name to the event flag group, which can be used by debuggers or μC/Probe to easily identify this event flag group. μC/OS-III stores a pointer to the name so there is no practical limit to its size, except that the ASCII string needs to be NUL terminated. L14-11(4) Initialize the flags inside the event flag group to zero (0) unless the task and ISRs signal events with bits cleared instead of bits set. If using cleared bits, initialize all the bits to ones (1). L14-11(5) OSFlagCreate() returns an error code based on the outcome of the call. If all the arguments are valid, err will contain OS_ERR_NONE. A task waits for one or more event flag bits either from an ISR or another task by calling OSFlagPend() as shown in Listing 14-12 (see Appendix A, “μC/OS-III API Reference Manual” on page 375 for details regarding the arguments). 282 Synchronization OS_FLAG_GRP MyEventFlagGrp; void MyTask (void *p_arg) { OS_ERR CPU_TS err; ts; : while (DEF_ON) { : OSFlagPend(&MyEventFlagGrp, (OS_FLAGS)0x0F, 10, OS_OPT_PEND_BLOCKING + OS_OPT_PEND_FLAG_SET_ANY, &ts, &err); /* Check “err” : : } /* (1) Pointer to event flag group /* Which bits to wait on /* Maximum time to wait */ */ */ /* /* /* */ */ */ */ Option(s) Timestamp of when posted to Pointer to Error returned (2) } Listing 14-12 Pending (or waiting) on an Event Flag Group L14-12(1) When called, OSFlagPend() starts by checking the arguments passed to this function to ensure they have valid values. If the bits the task is waiting for are set (or cleared depending on the option), OSFlagPend() returns and indicate which flags satisfied the condition. This is the outcome that the caller expects. If the event flag group does not contain the flags that the caller is looking for, the calling task might need to wait for the desired flags to be set (or cleared). If specifying OS_OPT_PEND_NON_BLOCKING as the option (the task is not to block), OSFlagPend() returns immediately to the caller and the returned error code indicates that the bits have not been set (or cleared). If specifying OS_OPT_PEND_BLOCKING as the option, the calling task will be inserted in the list of tasks waiting for the desired event flag bits. The task is not inserted in priority order but simply inserted at the beginning of the list. This is done because whenever bits are set (or cleared), it is necessary to examine all tasks in this list to see if their desired bits have been satisfied. 283 Chapter 14 If further specifying a non-zero timeout, the task will also be inserted in the tick list. A zero value for a timeout indicates that the calling task is willing to wait forever for the desired bits. The scheduler is then called since the current task is no longer able to run (it is waiting for the desired bits). The scheduler will run the next highest-priority task that is ready to run. When the event flag group is posted to and the task that called OSFlagPend() has its desired bits set or cleared, a task status is examined to determine the reason why OSFlagPend() is returning to its caller. The possibilities are: 1) The desired bits were set (or cleared) 2) The pend was aborted by another task 3) The bits were not set (or cleared) within the specified timeout 4) The event flag group was deleted When OSFlagPend() returns, the caller is notified of the above outcome through an appropriate error code. L14-12(2) If OSFlagPend() returns with err set to OS_ERR_NONE, assume that the desired bits were set (or cleared) and the task can proceed with servicing the ISR or task that created those events. If err contains anything else, OSFlagPend() either timed out (if the timeout argument was non-zero), the pend was aborted by another task or, the event flag group was deleted by another task. It is always important to examine the returned error code and not assume everything went as planned. To set (or clear) event flags (either from an ISR or a task), simply call OSFlagPost(), as shown in Listing 14-13. 284 Synchronization OS_FLAG_GRP MyEventFlagGrp; void MyISR (void) { OS_ERR err; : OSFlagPost(&MyEventFlagGrp, (1) (OS_FLAGS)0x0C, (2) OS_OPT_POST_FLAG_SET, (3) &err); (4) /* Check ’err” */ : : } Listing 14-13 Posting flags to an Event Flag Group L14-13(1) A task posts to the event flag group by calling OSFlagPost(). Specify the desired event flag group to post by passing its address. Of course, the event flag group must have been previously created. L14-13(2) The next argument specifies which bit(s) the ISR (or task) will be setting or clearing in the event flag group. L14-13(3) Specify OS_OPT_POST_FLAG_SET or OS_OPT_POST_FLAG_CLR. If specifying OS_OPT_POST_FLAG_SET, the bits specified in the second arguments will set the corresponding bits in the event flag group. For example, if MyEventFlagGrp.Flags contains 0x03, the code in Listing 14-13 will change MyEventFlagGrp.Flags to 0x0F. If specifying OS_OPT_POST_FLAG_CLR, the bits specified in the second arguments will clear the corresponding bits in the event flag group. For example, if MyEventFlagGrp.Flags contains 0x0F, the code in Listing 14-13 will change MyEventFlagGrp.Flags to 0x03. 285 Chapter 14 When calling OSFlagPost() specify as an option (i.e., OS_OPT_POST_NO_SCHED) to not call the scheduler. This means that the post is performed, but the scheduler is not called even if a higher-priority task was waiting for the event flag group. This allows the calling task to perform other post functions (if needed) and make all the posts take effect simultaneously. L14-13(4) OSFlagPost() returns an error code based on the outcome of the call. If the call was successful, err will contain OS_ERR_NONE. If not, the error code will indicate the reason of the error (see Appendix A, “μC/OS-III API Reference Manual” on page 375 for a list of possible error codes for OSFlagPost(). 14-4 SYNCHRONIZING MULTIPLE TASKS Synchronizing the execution of multiple tasks by broadcasting to a semaphore is a commonly used technique. It may be important to have multiple tasks start executing at the same time. Obviously, on a single processor, only one task will actually execute at one time. However, the start of their execution will be synchronized to the same time. This is called a multiple task rendezvous. However, some of the tasks synchronized might not be waiting for the semaphore when the broadcast is performed. It is fairly easy to resolve this problem by combining semaphores and event flags, as shown in Figure 14-11. For this to work properly, the task on the left needs to have a lower priority than the tasks waiting on the semaphore. 286 Synchronization 7DVN 7LPHRXW 26B6(0 7DVN 266HP3RVW %URDGFDVW  266HP3HQG  266HP3HQG 1 7DVN 7LPHRXW 266HP3HQG 26B)/$*B*53 7DVN 7LPHRXW 26)ODJ3HQG $1'&RQVXPH  26)ODJ3RVW  26)ODJ3RVW 26)ODJ3RVW Figure 14-11 Multiple Task Rendezvous F14-11(1) Each task that needs to synchronize at the rendezvous needs to set an event flag bit (and specify OS_OPT_POST_NO_SCHED). F14-11(2) The task needs to wait for the semaphore to be signaled. F14-11(3) The task that will be broadcasting must wait for “all” of the event flags corresponding to each task to be set. F14-11(4) When all waiting tasks are ready, the task that will synchronize the waiting task issues a broadcast to the semaphore. 287 Chapter 14 14-5 SUMMARY Three methods are presented to allow an ISR or a task to signal one or more tasks: semaphores, task semaphores, and event flags. Both semaphores and task semaphores contain a counter allowing them to perform credit tracking and accumulate the occurrence of events. If an ISR or task needs to signal a single task (as opposed to multiple tasks when the event occurs), it makes sense to use a task semaphore since it prevents the user from having to declare an external semaphore object. Also, task semaphore services are slightly faster (in execution time) than semaphores. Event flags are used when a task needs to synchronize with the occurrence of one or more events. However, event flags cannot perform credit tracking since a single bit (as opposed to a counter) represents each event. 288 Chapter 15 Message Passing It is sometimes necessary for a task or an ISR to communicate information to another task. This information transfer is called inter-task communication. Information can be communicated between tasks in two ways: through global data, or by sending messages. As seen in Chapter 13, “Resource Management” on page 209, when using global variables, each task or ISR must ensure that it has exclusive access to variables. If an ISR is involved, the only way to ensure exclusive access to common variables is to disable interrupts. If two tasks share data, each can gain exclusive access to variables either by disabling interrupts, locking the scheduler, using a semaphore, or preferably, using a mutual-exclusion semaphore. Note that a task can only communicate information to an ISR by using global variables. A task is not aware when a global variable is changed by an ISR, unless the ISR signals the task, or the task polls the contents of a variable periodically. Messages can either be sent to an intermediate object called a message queue, or directly to a task since in μC/OS-III, each task has its own built-in message queue. Use an external message queue if multiple tasks are to wait for messages. Send a message directly to a task if only one task will process the data received. When a task waits for a message to arrive, it does not consume CPU time. 289 Chapter 15 15-1 MESSAGES A message consists of a pointer to data, a variable containing the size of the data being pointed to, and a timestamp indicating when the message was sent. The pointer can point to a data area or even a function. Obviously, the sender and the receiver must agree as to the contents and the meaning of the message. In other words, the receiver of the message will know the meaning of the message received to be able to process it. For example, an Ethernet controller receives a packet and sends a pointer to this packet to a task that knows how to handle the packet. The message contents must always remain in scope since the data is actually sent by reference instead of by value. In other words, data sent is not copied. Consider using dynamically allocated memory as described in Chapter 17, “Memory Management” on page 323. Alternatively, pass pointers to a global variable, a global data structure, a global array, or a function, etc. 15-2 MESSAGE QUEUES A message queue is a kernel object allocated by the application. In fact, you can allocate any number of message queues. The only limit is the amount of RAM available. There are a number of operations that the user can perform on message queues, summarized in Figure 15-1. However, an ISR can only call OSQPost(). A message queue must be created before sending messages through it. 7DVN 264&UHDWH 264'HO 264)OXVK 2643HQG$ERUW 2643RVW 0HVVDJH4XHXH 2643HQG 7DVN 2643RVW 7LPHRXW ,65 6L]H Figure 15-1 Operations on message queue 290 Message Passing Message queues are drawn as a first-in, first-out pipe (FIFO). However, with μC/OS-III, it is possible to post messages in last-in, first-out order (LIFO). The LIFO mechanism is useful when a task or an ISR must send an “urgent” message to a task. In this case, the message bypasses all other messages already in the message queue. The size of the message queue is configurable at run time. The small hourglass close to the receiving task indicates that the task has an option to specify a timeout. This timeout indicates that the task is willing to wait for a message to be sent to the message queue within a certain amount of time. If the message is not sent within that time, μC/OS-III resumes the task and returns an error code indicating that the task was made ready to run because of a timeout, and not because the message was received. It is possible to specify an infinite timeout and indicate that the task is willing to wait forever for the message to arrive. The message queue also contains a list of tasks waiting for messages to be sent to the message queue. Multiple tasks can wait on a message queue as shown in Figure 15-2. When a message is sent to the message queue, the highest priority task waiting on the message queue receives the message. Optionally, the sender can broadcast a message to all tasks waiting on the message queue. In this case, if any of the tasks receiving the message from the broadcast has a higher priority than the task sending the message (or interrupted task, if the message is sent by an ISR), μC/OS-III will run the highest-priority task that is waiting. Notice that not all tasks must specify a timeout; some tasks may want to wait forever. Task Task OSQCreate() OSQDel () OSQFlush() OSQPendAbort() OSQPost() Message Queue OSQPend() Task OSQPend() OSQPost() OSQPend() Timeout ISR Task Figure 15-2 Multiple tasks waiting on a message queue 291 Chapter 15 15-3 TASK MESSAGE QUEUE It is fairly rare to find applications where multiple tasks wait on a single message queue. Because of this, a message queue is built into each task and the user can send messages directly to a task without going through an external message queue object. This feature not only simplifies the code but, is also more efficient than using a separate message queue object. The message queue that is built into each task is shown in Figure 15-3. 7DVN 267DVN4)OXVK 267DVN43HQG$ERUW 267DVN43RVW 267DVN43HQG 7DVN 267DVN43RVW ,65 7LPHRXW Figure 15-3 Task message queue Task message queue services in μC/OS-III start with the OSTaskQ???() prefix, and services available to the application programmer are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. Setting OS_CFG_TASK_EN in OS_CFG.H enables task message queue services. The code for task message queue management is found in OS_TASK.C. Use this feature if the code knows which task to send the message(s) to. For example, if receiving an interrupt from an Ethernet controller, send the address of the received packet to the task that will be responsible for processing the received packet. 292 Message Passing 15-4 BILATERAL RENDEZVOUS Two tasks can synchronize their activities by using two message queues, as shown in Figure 15-4. This is called a bilateral rendezvous and works the same as with semaphores except that both tasks may send messages to each other. A bilateral rendezvous cannot be performed between a task and an ISR since an ISR cannot wait on a message queue. 0HVVDJH4XHXH 2643RVW 2643HQG 7LPHRXW 7DVN 7DVN 2643HQG 0HVVDJH4XHXH 2643RVW 7LPHRXW Figure 15-4 Bilateral Rendezvous In a bilateral rendezvous, each message queue holds a maximum of one message. Both message queues are initially created empty. When the task on the left reaches the rendezvous point, it sends a message to the top message queue and waits for a message to arrive on the bottom message queue. Similarly, when the task on the right reaches its rendezvous point, it sends a message to the message queue on the bottom and waits for a message to arrive on the top message queue. Figure 15-5 shows how to use task-message queues to perform a bilateral rendezvous. 293 Chapter 15 0HVVDJH4XHXH 7DVN 267DVN43HQG 267DVN43RVW 267DVN43RVW 7DVN 267DVN43HQG 0HVVDJH4XHXH Figure 15-5 Figure Bilateral Rendezvous with task message queues 15-5 FLOW CONTROL Task-to-task communication often involves data transfer from one task to another. One task produces data while the other consumes it. However, data processing takes time and consumers might not consume data as fast as it is produced. In other words, it is possible for the producer to overflow the message queue if a higher-priority task preempts the consumer. One way to solve this problem is to add flow control in the process as shown in Figure 15-6. 0HVVDJH4XHXH 2643RVW 2643HQG 7DVN 7DVN 266HP3HQG 266HP3RVW 1 Figure 15-6 Producer and consumer tasks with flow control 294 Message Passing Here, a counting semaphore is used, initialized with the number of allowable messages that can be sent by the consumer. If the consumer cannot queue more than 10 messages, the counting semaphore contains a count of 10. As shown in the pseudo code of Listing 15-1, the producer must wait on the semaphore before it is allowed to send a message. The consumer waits for messages and, when processed, signals the semaphore. Producer Task: Pend on Semaphore; Send message to message queue; Consumer Task: Wait for message from message queue; Signal the semaphore; Listing 15-1 Producer and consumer flow control Combining the task message queue and task semaphores (see Chapter 14, “Synchronization” on page 251), it is easy to implement flow control as shown in Figure 15-7. In this case, however, OSTaskSemSet() must be called immediately after creating the task to set the value of the task semaphore to the same value as the maximum number of allowable messages in the task message queue. 295 Chapter 15 &DOO 267DVN6HP6HW WRVHWTXHXHVL]H DIWHUWDVNFUHDWLRQ 267DVN6HP3HQG 267DVN6HP3RVW 7DVN 1 267DVN43RVW 267DVN43HQG 7DVN 0HVVDJH4XHXH Figure 15-7 Flow control with task semaphore and task message queue 15-6 KEEPING THE DATA IN SCOPE The messages sent typically point to data structures, variables, arrays, tables, etc. However, it is important to realize that the data must remain static until the receiver of the data completes its processing of the data. Once sent, the sender must not touch the sent data. This seems obvious, however it is easy to forget. One possibility is to use the fixed-size memory partition manager provided with μC/OS-III (see Chapter 17, “Memory Management” on page 323) to dynamically allocate and free memory blocks used to pass the data. Figure 15-8 shows an example. For sake of illustration, assume that a device is sending data bytes to the UART in packets using a protocol. In this case, the first byte of a packet is unique and the end-of-packet byte is also unique. 296 Message Passing   8$57  0HVVDJH4XHXH 8$57 5[ ,65 2643RVW  2643HQG  8$57 5[ 7DVN 7LPHRXW 260HP*HW  260HP3XW 0HPRU\3DUWLWLRQ  Figure 15-8 Using memory partitions for message contents F15-8(1) Here, a UART generates an interrupt when characters are received. F15-8(2) The pseudo-code in Listing 15-2 shows what the UART ISR code might look like. There are a lot of details omitted for sake of simplicity. The ISR reads the byte received from the UART and sees if it corresponds to a start of packet. If it is, a buffer is obtained from the memory partition. F15-8(3) The received byte is then placed in the buffer. F15-8(4) If the data received is an end-of-packet byte, simply post the address of the buffer to the message queue so that the task can process the received packet. F15-8(5) If the message sent makes the UART task the highest priority task, μC/OS-III will switch to that task at the end of the ISR instead of returning to the interrupted task. The task retrieves the packet from the message queue. Note that the OSQPend() call also returns the number of bytes in the packet and a time stamp indicating when the message was sent. F15-8(6) When the task is finished processing the packet, the buffer is returned to the memory partition it came from by calling OSMemPut(). 297 Chapter 15 void UART_ISR (void) { OS_ERR err; RxData = Read byte from UART; if (RxData == Start of Packet) { RxDataPtr = OSMemGet(&UART_MemPool, &err); RxDataCtr = 0; } else { RxDataCtr++; } if (RxData == End of Packet byte) { OSQPost((OS_Q *)&UART_Q, (void *)RxDataPtr, (OS_MSG_SIZE)RxDataCtr, (OS_OPT )OS_OPT_POST_FIFO, (OS_ERR *)&err); RxDataPtr = NULL; RxDataCtr = 0; } else; { *RxDataPtr++ = RxData; } /* See if we need a new buffer */ /* Yes */ /* Update number of bytes received */ /* See if we got a full packet */ /* Yes, post to task for processing */ /* Don’t point to sent buffer /* Save the byte received */ */ } Listing 15-2 UART ISR Pseudo-code 298 Message Passing 15-7 USING MESSAGE QUEUES Table 15-1 shows a summary of message-queue services available from μC/OS-III. Refer to Appendix A, “μC/OS-III API Reference Manual” on page 375 for a full description on their use. Function Name Operation OSQCreate() Create a message queue. OSQDel() Delete a message queue. OSQFlush() Empty the message queue. OSQPend() Wait for a message. OSQPendAbort() Abort waiting for a message. OSQPost() Send a message through a message queue. Table 15-1 Message queue API summary Table 15-2 is a summary of task message queue services available from μC/OS-III. Refer to Appendix A, “μC/OS-III API Reference Manual” on page 375, for a full description on how to their use. Function Name Operation OSTaskQPend() Wait for a message. OSTaskQPendAbort() Abort the wait for a message. OSTaskQPost() Send a message to a task. OSTaskQFlush() Empty the message queue. Table 15-2 Task message queue API summary 299 Chapter 15 Figure 15-9 shows an example of using a message queue when determining the speed of a rotating wheel.  3UHYLRXV &RXQWV 0HVVDJH 4XHXH  5RWDWLQJ :KHHO  6HQVRU  5HIHUHQFH )UHTXHQF\ ,65 2643RVW  &XUUHQW &RXQWV ELW ,QSXW&DSWXUH  2643HQG  7DVN 7LPHRXW  530 $YHUDJH530 0D[LPXP530 8QGHUVSHHG 2YHUVSHHG Figure 15-9 Measuring RPM F15-9(1) The goal is to measure the RPM of a rotating wheel. F15-9(2) A sensor is used to detect the passage of a hole in the wheel. In fact, to receive additional resolution, the wheel could contain multiple holes that are equally spaced. F15-9(3) A 32-bit input capture register is used to capture the value of a free-running counter when the hole is detected. F15-9(4) An interrupt is generated when the hole is detected. The ISR reads the current count of the input capture register and subtracts the value of the previous capture to receive the time it took for one rotation (assuming only a single hole). Delta Counts = Current Counts – Previous Counts; Previous Counts = Current Counts; 300 Message Passing F15-9(5) The delta counts are sent to a message queue. Since a message is actually a pointer, if the pointer is 32-bits wide on the processor in use, simply cast the 32-bit delta counts to a pointer and send this through the message queue. A safer and more portable approach is to dynamically allocate storage to hold the delta counts using a memory block from μC/OS-III’s memory management services (see Chapter 17, “Memory Management” on page 323) and send the address of the allocated memory block. F15-9(6) When the message is sent, the RPM measurement task wakes up and computes the RPM as follows: RPM = 60 * Reference Frequency / Delta Counts; The user may specify a timeout on the pend call and the task will wake up if a message is not sent within the timeout period. This allows the user to easily detect that the wheel is not rotating and therefore, the RPM is 0. F15-9(7) Along with computing RPM, the task can also compute average RPM, maximum RPM, and whether the speed is above or below thresholds, etc. A few interesting things are worth noting about the above example. First, the ISR is very short; read the input capture and post the delta counts to the task to accomplish the time-consuming math. Second, with the timeout on the pend, it is easy to detect that the wheel is stopped. Finally, the task can perform additional calculations and can further detect such errors as the wheel spinning too fast or too slow. In fact, the task can notify other tasks about these errors, if needed. Listing 15-3 shows how to implement the RPM measurement example using μC/OS-III’s message queue services. Some of the code is pseudo-code, while the calls to μC/OS-III services are actual calls with their appropriate arguments. 301 Chapter 15 OS_Q RPM_Q; CPU_INT32U CPU_INT32U CPU_INT32U DeltaCounts; CurrentCounts; PreviousCounts; (1) void main (void) { OS_ERR : err ; OSInit(&err) ; : OSQCreate((OS_Q *)&RPM_Q, (CPU_CHAR *)”My Queue”, (OS_MSG_QTY)10, (OS_ERR *)&err); (2) : OSStart(&err); } void RPM_ISR (void) { OS_ERR err; Clear the interrupt from the sensor; CurrentCounts = Read the input capture; DeltaCounts = CurrentCounts – PreviousCounts; PreviousCounts = CurrentCounts; OSQPost((OS_Q *)&RPM_Q, (void *)DeltaCounts, (OS_MSG_SIZE)sizeof(void *), (OS_OPT )OS_OPT_POST_FIFO, (OS_ERR *)&err); } 302 (3) (4) Message Passing void RPM_Task (void *p_arg) { CPU_INT32U OS_ERR OS_MSG_SIZE delta; err; size; CPU_TS ts; DeltaCounts = 0; PreviousCounts = 0; CurrentCounts = 0; while (DEF_ON) { delta = (CPU_INT32U)OSQPend((OS_Q (OS_TICK (OS_OPT (OS_MSG_SIZE (CPU_TS (OS_ERR if (err == OS_ERR_TIMEOUT) { RPM = 0; } else { if (delta > 0u) { RPM = 60 * Reference Frequency / } } Compute average RPM; Detect maximum RPM; Check for overspeed; Check for underspeed; : : } *)&RPM_Q, (5) )OS_CFG_TICK_RATE * 10, )OS_OPT_PEND_BLOCKING, *)&size, *)&ts, *)&err); (6) delta; (7) (8) } Listing 15-3 Pseudo-code of RPM measurement L15-3(1) Variables are declared. Notice that it is necessary to allocate storage for the message queue itself. L15-3(2) Call OSInit() and create the message queue before it is used. The best place to do this is in startup code. 303 Chapter 15 L15-3(3) The RPM ISR clears the sensor interrupt and reads the value of the 32-bit input capture. Note that it is possible to read RPM if there is only a 16-bit input capture. The problem with a 16-bit input capture is that it is easy for it to overflow, especially at low RPMs. The RPM ISR also computes delta counts directly in the ISR. It is just as easy to post the current counts and let the task compute the delta. However, the subtraction is a fast operation and does not significantly increase ISR processing time. L15-3(4) Send the delta counts to the RPM task, responsible for computing the RPM and perform additional computations. Note that the message gets lost if the queue is full when the user attempts to post. This happens if data is generated faster than it is processed. Unfortunately, it is not possible to implement flow control in the example because it is dealing with an ISR. L15-3(5) The RPM task starts by waiting for a message from the RPM ISR by pending on the message queue. The third argument specifies the timeout. In this case, ten seconds worth of timeout is specified. However, the value chosen depends on the requirements of an application. Also notice that the ts variable contains the timestamp of when the post was completed. Determine the time it took for the task to respond to the message received by calling OS_TS_GET(), and subtract the value of ts: response_time = OS_TS_GET() – ts; L15-3(6) If a timeout occurs, assume the wheel is no longer spinning. L15-3(7) The RPM is computed from the delta counts received, and from the reference frequency of the free-running counter. L15-3(8) Additional computations are performed as needed. In fact, messages can be sent to different tasks in case error conditions are detected. The messages would be processed by the other tasks. For example, if the wheel spins too fast, another task can initiate a shutdown on the device that is controlling the wheel speed. 304 Message Passing In Listing 15-4, OSQPost() and OSQPend() are replaced with OSTaskQPost() and OSTaskQPend() for the RPM measurement example. Notice that the code is slightly simpler to use and does not require creating a separate message queue object. However, when creating the RPM task, it is important to specify the size of the message queue used by the task and compile the application code with OS_CFG_TASK_Q_EN set to 1. The differences between using message queues and the task’s message queue will be explained. OS_TCB OS_STK CPU_INT32U CPU_INT32U CPU_INT32U RPM_TCB; RPM_Stk[1000]; DeltaCounts ; CurrentCounts ; PreviousCounts ; void main (void) { OS_ERR err ; : OSInit(&err) ; : void OSTaskCreate ((OS_TCB (CPU_CHAR (OS_TASK_PTR (void (OS_PRIO (1) *)&RPM_TCB, *)”RPM Task”, (2) )RPM_Task, *)0, )10, (CPU_STK *)&RPM_Stk[0], (CPU_STK_SIZE )100, (CPU_STK_SIZE )1000, (OS_MSG_QTY )10, (OS_TICK )0, (void *)0, (OS_OPT )(OS_OPT_TASK_STK_CHK + OS_OPT_TASK_STK_CLR), (OS_ERR *)&err); : OSStart(&err); } 305 Chapter 15 void RPM_ISR (void) { OS_ERR err; Clear the interrupting from the sensor; CurrentCounts = Read the input capture; DeltaCounts = CurrentCounts – PreviousCounts; PreviousCounts = CurrentCounts; OSTaskQPost((OS_TCB *)&RPM_TCB, (void *)DeltaCounts, (OS_MSG_SIZE)sizeof(DeltaCounts), (OS_OPT )OS_OPT_POST_FIFO, (OS_ERR *)&err); (3) } void RPM_Task (void *p_arg) { CPU_INT32U delta; OS_ERR err; OS_MSG_SIZE size; CPU_TS ts; DeltaCounts = 0; PreviousCounts = 0; CurrentCounts = 0; while (DEF_ON) { delta = (CPU_INT32U)OSTaskQPend((OS_TICK )OS_CFG_TICK_RATE * 10, (OS_OPT )OS_OPT_PEND_BLOCKING, (OS_MSG_SIZE *)&size, (CPU_TS *)&ts, (OS_ERR *)&err); if (err == OS_ERR_TIMEOUT) { RPM = 0; } else { if (delta > 0u) { RPM = 60 * ReferenceFrequency / delta; } } Compute average RPM; Detect maximum RPM; Check for overspeed; Check for underspeed; : : } (4) } Listing 15-4 Pseudo-code of RPM measurement 306 Message Passing L15-4(1) Instead of declaring a message queue, it is important to know the OS_TCB of the task that will be receiving messages. L15-4(2) The RPM task is created and a queue size of 10 entries is specified. Of course, hard-coded values should not be specified in a real application, but instead, use #defines. Fixed numbers are used here for sake of illustration. L15-4(3) Instead of posting to a message queue, the ISR posts the message directly to the task, specifying the address of the OS_TCB of the task. This is known since the OS_TCB is allocated when creating the task. L15-4(4) The RPM task starts by waiting for a message from the RPM ISR by calling OSTaskQPend(). This is an inherent call so it is not necessary to specify the address of the OS_TCB to pend on as the current task is assumed. The second argument specifies the timeout. Here, ten seconds worth of timeout is specified, which corresponds to 6 RPM. 15-8 CLIENTS AND SERVERS Another interesting use of message queues is shown in Figure 15-10. Here, a task (the server) is used to monitor error conditions that are sent to it by other tasks or ISRs (clients). For example, a client detects whether the RPM of the rotating wheel has been exceeded, another client detects whether an over-temperature exists, and yet another client detects that a user pressed a shutdown button. When the clients detect error conditions, they send a message through the message queue. The message sent indicates the error detected, which threshold was exceeded, the error code that is associated with error conditions, or even suggests the address of a function that will handle the error, and more. Task OSQPost() Task OSQPost() Message Queue OSQPend() OSQPost() ISR Error Handler Task Timeout Figure 15-10 Clients and Servers 307 Chapter 15 15-9 MESSAGE QUEUES INTERNALS As previously described, a message consists of a pointer to actual data, a variable indicating the size of the data being pointed to and a timestamp indicating when the message was actually sent. When sent, a message is placed in a data structure of type OS_MSG, shown in Figure 15-11. The sender and receiver are unaware of this data structure since everything is hidden through the APIs provided by μC/OS-III. 26B06* 3RLQWHUWRQH[W26B06* 1H[W3WU 0VJ6L]H 0VJ76 0VJ3WU 3RLQWHUWRPHVVDJHFRQWHQWV Figure 15-11 OS_MSG structure μC/OS-III maintains a pool of free OS_MSGs. The total number of available messages in the pool is determined by the value of OS_CFG_MSG_POOL_SIZE found in OS_CFG_APP.H. When μC/OS-III is initialized, OS_MSGs are linked in a single linked list as shown in Figure 15-12. Notice that the free list is maintained by a data structure of type OS_MSG_POOL, which contains three fields: .NextPtr, which points to the free list; .NbrFree, which contains the number of free OS_MSGs in the pool; and finally .NbrUsed, which contains the number of OS_MSGs allocated to application. 26B06*B322/ 26B06* 26B06* 26B06* 1H[W3WU 1H[W3WU 1H[W3WU 1H[W3WU 1EU)UHH 0VJ6L]H 0VJ6L]H 0VJ6L]H 1EU8VHG 0VJ76 0VJ76 0VJ76 0VJ3WU 0VJ3WU 0VJ3WU 18// 26B&)*B06*B322/B6,=( Figure 15-12 Pool of free OS_MSGs 308 Message Passing Messages are queued using a data structure of type OS_MSG_Q, as shown in Figure 15-13. 26B06*B4 ,Q3WU 2XW3WU 3RLQWHUWRQH[W26B06*WRLQVHUW 3RLQWHUWRQH[W26B06*WRUHPRYH 1EU(QWULHV6L]H 1EU(QWULHV 1EU(QWULHV0D[ Figure 15-13 OS_MSG_Q structure .InPtr This field contains a pointer to where the next OS_MSG will be inserted in the queue. In fact, the OS_MSG will be inserted “after” the OS_MSG pointed to. .OutPtr This field contains a pointer to where the next OS_MSG will be extracted. .NbrEntriesSize This field contains the maximum number of OS_MSGs that the queue will hold. If an application attempts to send a message and the .NbrEntries matches this value, the queue is considered to be full and the OS_MSG will not be inserted. .NbrEntries This field contains the current number of OS_MSGs in the queue. .NbrEntriesMax This field contains the highest number of OS_MSGs existing in the queue at any given time. A number of internal functions are used by μC/OS-III to manipulate the free list and messages. Specifically, OS_MsgQPut() inserts an OS_MSG in an OS_MSG_Q, OS_MsgQGet() extracts an OS_MSG from an OS_MSG_Q, and OS_MsgQFreeAll() returns all OS_MSGs in an OS_MSG_Q to the pool of free OS_MSGs. There are other OS_MsgQ??() functions in OS_MSG.C that are used during initialization. 309 Chapter 15 Figure 15-14 shows an example of an OS_MSG_Q when four OS_MSGs are inserted. 26B06*B4 ,Q3WU 2XW3WU 1EU(QWULHV6L]H 1EU(QWULHV 1EU(QWULHV0D[  26B06*   26B06* 26B06* 26B06* 1H[W3WU 1H[W3WU 1H[W3WU 1H[W3WU 0VJ6L]H 0VJ6L]H 0VJ6L]H 0VJ6L]H 0VJ76 0VJ76 0VJ76 0VJ76 0VJ3WU 0VJ3WU 0VJ3WU 0VJ3WU 'DWD 'DWD 'DWD 'DWD 18// Figure 15-14 OS_MSG_Q with four OS_MSGs OS_MSG_Qs are used inside two additional data structures: OS_Q and OS_TCB. Recall that an OS_Q is declared when creating a message queue object. An OS_TCB is a task control block and, as previously mentioned, each OS_TCB can have its own message queue when the configuration constant OS_CFG_TASK_Q_EN is set to 1 in OS_CFG.H. Figure 15-15 shows the contents of an OS_Q and partial contents of an OS_TCB containing an OS_MSG_Q. The OS_MSG_Q data structure is shown as an “exploded view” to emphasize the structure within the structure. 26B4 26B7&% 7\SH 6WN3WU 1DPH3WU  3HQG/LVW  0VJ4 26B06*B4 0VJ4 26B06*B4 ,Q3WU  ,Q3WU 2XW3WU  2XW3WU 1EU(QWULHV6L]H  1EU(QWULHV6L]H 1EU(QWULHV 1EU(QWULHV 1EU(QWULHV0D[ 1EU(QWULHV0D[ Figure 15-15 OS_Q and OS_TCB each contain an OS_MSG_Q 310 Message Passing 15-10 SUMMARY Message queues are useful when a task or an ISR is to send data to another task. The data sent must remain in scope as it is actually sent by reference instead of by value. In other words, the data sent is not copied. The task waiting for the data will not consume CPU time while waiting for a message to be sent to it. If it is known which task is responsible for servicing messages sent by producers, use task message queue (i.e., OSTaskQ???()) services since they are simple and fast. Task message queue services are enabled when OS_CFG_TASK_Q_EN is set to 1 in OS_CFG.H. If multiple tasks must wait for messages from the same message queue, allocate an OS_Q and have the tasks wait for messages to be sent to the queue. Alternatively, broadcast special messages to all tasks waiting on a message queue. Regular message queue services are enabled when OS_CFG_Q_EN is set to 1 in OS_CFG.H. Messages are sent using an OS_MSG data structure obtained by μC/OS-III from a pool. Set the maximum number of messages that can be sent to a message queue, or as many messages as are available in the pool. 311 Chapter 15 312 Chapter 16 Pending On Multiple Objects In Chapter 10, “Pend Lists (or Wait Lists)” on page 177 we saw how multiple tasks can pend (or wait) on a single kernel object such as a semaphore, mutual exclusion semaphore, event flag group, or message queue. In this chapter, we will see how tasks can pend on multiple objects. However, μC/OS-III only allows for pend on multiple semaphores and/or message queues. In other words, it is not possible to pend on multiple event flag groups or mutual exclusion semaphores. As shown in Figure 16-1, a task can pend on any number of semaphores or message queues at the same time. The first semaphore or message queue posted will make the task ready to run and compete for CPU time with other tasks in the ready list. As shown, a task pends on multiple objects by calling OSPendMulti() and specifies an optional timeout value. The timeout applies to all of the objects. If none of the objects are posted within the specified timeout, the task resumes with an error code indicating that the pend timed out. 313 Chapter 16 6HPDSKRUH 266HP3RVW 6HPDSKRUH 263HQG0XOWL 266HP3RVW 0HVVDJH 4XHXH 7DVN 2643RVW 0HVVDJH 4XHXH 2643RVW 7LPHRXW 6HPDSKRUH 266HP3RVW Figure 16-1 Task pending on multiple objects 314 Pending On Multiple Objects Listing 16-1 shows the function prototype of OSPendMulti() and Figure 16-2 exhibits an array of OS_PEND_DATA elements. OS_OBJ_QTY OSPendMulti (OS_PEND_DATA OS_OBJ_QTY OS_TICK OS_OPT OS_ERR *p_pend_data_tbl, (1) tbl_size, timeout, opt, *p_err); (2) (3) (4) (5) Listing 16-1 OSPendMulti() prototype >@ >@ >@ 3UHY3WU 1H[W3WU 7&%3WU 3HQG2EM3WU 5G\2EM3WU 5G\0VJ3WU 5G\0VJ6L]H 5G\76 3UHY3WU 1H[W3WU 7&%3WU 3HQG2EM3WU 5G\2EM3WU 5G\0VJ3WU 5G\0VJ6L]H 5G\76 3UHY3WU 1H[W3WU 7&%3WU 3HQG2EM3WU 5G\2EM3WU 5G\0VJ3WU 5G\0VJ6L]H 5G\76 >1@ 3UHY3WU 1H[W3WU 7&%3WU 3HQG2EM3WU 5G\2EM3WU 5G\0VJ3WU 5G\0VJ6L]H 5G\76 26B3(1'B'$7$ Figure 16-2 Array of OS_PEND_DATA L16-1(1) OSPendMulti() is passed an array of OS_PEND_DATA elements. The caller must instantiate an array of OS_PEND_DATA. The size of the array depends on the total number of kernel objects that the task wants to pend on. For example, if the task wants to pend on three semaphores and two message queues then the array contains five OS_PEND_DATA elements as shown below: OS_PEND_DATA my_pend_multi_tbl[5]; 315 Chapter 16 The calling task needs to initialize the .PendObjPtr of each element of the array to point to each of the objects to be pended on. For example: OS_SEM MySem1; OS_SEM OS_SEM OS_Q OS_Q MySem2; MySem3; MyQ1; MyQ2; void MyTask (void) { OS_ERR err; OS_PEND_DATA my_pend_multi_tbl[5]; : while (DEF_ON) { : my_pend_multi_tbl[0].PendObjPtr = (OS_PEND_OBJ)&MySem1; my_pend_multi_tbl[1].PendObjPtr = (OS_PEND_OBJ)&MySem2; my_pend_multi_tbl[2].PendObjPtr = (OS_PEND_OBJ)&MySem3; my_pend_multi_tbl[3].PendObjPtr = (OS_PEND_OBJ)&MyQ1; my_pend_multi_tbl[4].PendObjPtr = (OS_PEND_OBJ)&MyQ2; OSPendMulti((OS_PEND_DATA *)&my_pend_multi_tbl[0], (OS_OBJ_QTY )5, (OS_TICK )0, (OS_OPT (OS_ERR /* Check ’err” */ : (6) )OS_OPT_PEND_BLOCKING, *)&err); } } L16-1(2) This argument specifies the size of the OS_PEND_DATA table. In the above example, this is 5. L16-1(3) Specify whether or not to timeout in case none of the objects are posted within a certain amount of time. A non-zero value indicates the number of ticks to timeout. Specifying zero indicates the task will wait forever for any of the objects to be posted. L16-1(4) The “opt” argument specifies whether to wait for objects to be posted (set opt to OS_OPT_PEND_BLOCKING) or, not block if none of the objects have already been posted (set opt to OS_OPT_PEND_NON_BLOCKING). 316 Pending On Multiple Objects F16-2(1) As with most μC/OS-III function calls, specify the address of a variable that will receive an error code based on the outcome of the function call. See Appendix A, “μC/OS-III API Reference Manual” on page 375 for a list of possible error codes. As always, it is highly recommended to examine the error return code. F16-2(2) Note that all objects are cast to OS_PEND_OBJ data types. When called, OSPendMulti() first starts by validating that all of the objects specified in the OS_PEND_DATA table are either an OS_SEM or an OS_Q. If not, an error code is returned. Next, OSPendMulti() goes through the OS_PEND_DATA table to see if any of the objects have already posted. If so, OSPendMulti() fills the following fields in the table: .RdyObjPtr, .RdyMsgPtr, .RdyMsgSize and .RdyTS. .RdyObjPtr is a pointer to the object if the object has been posted. For example, if the first object in the table is a semaphore and the semaphore has been posted to, my_pend_multi_tbl[0].RdyObjPtr is set to my_pend_multi_tbl[0].PendObjPtr. .RdyMsgPtr is a pointer to a message if the object in the table at this entry is a message queue and a message was received from the message queue. .RdyMsgSize is the size of the message received if the object in the table at this entry is a message queue and a message was received from the message queue. .RdyTS is the timestamp of when the object posted. This allows the user to know when a semaphore or message queue posts. If there are no objects posted, then OSPendMulti() places the current task in the wait list of all the objects that it is pending on. This is a complex and tedious process for OSPendMulti() since there can be other tasks in the pend list of some of these objects we are pending on. To indicate how tricky things get, Figure 16-3 is an example of a task pending on two semaphores. 317 Chapter 16 26B6(0  7\SH 26B3(1'B2%- 1DPH3WU   7DLO3WU +HDG3WU  &WU 26B3(1'B/,67 76 26B3(1'B'$7$   1H[W3WU 7&%3WU 26B6(0 7\SH 26B3(1'B2%- 26B3(1'B/,67 7DLO3WU +HDG3WU   3HQG2EM3WU  5G\2EM3WU 5G\0VJ3WU 1DPH3WU   >@ 3UHY3WU 5G\0VJ6L]H  5G\76 &WU  76 >@ 3UHY3WU 1H[W3WU 7&%3WU    3HQG2EM3WU 5G\2EM3WU 5G\0VJ3WU  5G\0VJ6L]H 5G\76 3HQG'DWD7EO3WU 3HQG'DWD7EO(QWULHV   26B7&% Figure 16-3 Task pending on two semaphores F16-3(1) A pointer to the base address of the OS_PEND_DATA table is placed in the OS_TCB of the task placed in the pend list of the two semaphores. F16-3(2) The numbers of entries in the OS_PEND_DATA table are also placed in the OS_TCB. Again, this task is waiting on two semaphores and therefore there are two entries in the table. 318 Pending On Multiple Objects F16-3(3) Entry [0] of the OS_PEND_DATA table is linked to the semaphore object specified by that entry’s .PendObjPtr. F16-3(4) This pointer was specified by the caller of OSPendMulti(). F16-3(5) Since there is only one task in the pend list of the semaphore, the .PrevPtr and .NextPtr are pointing to NULL. F16-3(6) The second semaphore points to the second entry in the OS_PEND_DATA table. F16-3(7) This pointer was specified by the caller of OSPendMulti(). F16-3(8) The second semaphore only has one entry in its pend list. Therefore the .PrevPtr and .NextPtr both point to NULL. F16-3(9) OSPendMulti() links back each OS_PEND_DATA entry to the task that is waiting on the two semaphores. Figure 16-4 is a more complex example where one task is pending on two semaphores while another task also pends on one of the two semaphores. The examples presented so far only show semaphores, but they could be combinations of semaphores and message queues. 319 Chapter 16 26B6(0 7\SH 1DPH3WU   7DLO3WU +HDG3WU &WU 76 26B3(1'B'$7$  3UHY3WU 1H[W3WU >@  7&%3WU 26B6(0 3HQG2EM3WU 7\SH 5G\2EM3WU 1DPH3WU 5G\0VJ3WU   5G\0VJ6L]H 7DLO3WU 5G\76 +HDG3WU &WU 76  3UHY3WU >@ 26B3(1'B'$7$ 3UHY3WU 1H[W3WU 1H[W3WU 7&%3WU 7&%3WU 3HQG2EM3WU 3HQG2EM3WU 5G\2EM3WU 5G\2EM3WU 5G\0VJ3WU 5G\0VJ3WU 5G\0VJ6L]H 5G\0VJ6L]H 5G\76 5G\76 3HQG'DWD7EO3WU 3HQG'DWD7EO(QWULHV  3HQG'DWD7EO3WU  3HQG'DWD7EO(QWULHV 26B7&% 26B7&% +LJK 3ULRULW\ 7DVN /RZ 3ULRULW\ 7DVN  Figure 16-4 Tasks pending on semaphores 320 Pending On Multiple Objects When either an ISR or a task signals or sends a message to one of the objects that the task is pending on, OSPendMulti() returns, indicating in the OS_PEND_DATA table which object was posted. This is done by only filling in “one” of the .RdyObjPtr entries, the one that corresponds to the object posted as shown in Figure 16-2. Only one of the entries in the OS_PEND_DATA table will have a .RdyObjPtr with a non-NULL value while all the other entries have the .RdyObjPtr set to NULL. Going back to the case where a task waits on five semaphores and two message queues, if the first message queue is posted while the task is pending on all those objects, the OS_PEND_DATA table will be as shown in Figure 16-5. >@ >@ >@ >@ >@ 3UHY3WU 1H[W3WU 7&%3WU 3HQG2EM3WU 5G\2EM3WU 5G\0VJ3WU 5G\0VJ6L]H 5G\76   7&%3WU 3HQG2EM3WU       7&%3WU 3HQG2EM3WU       7&%3WU 3HQG2EM3WU       7&%3WU 0\4 0\4 0VJ 3WU 0VJ 6L]H 7LPHVWDPS   7&%3WU 3HQG2EM3WU     Figure 16-5 Message queue #1 posted before timeout expired 16-1 SUMMARY μC/OS-III allows tasks to pend on multiple kernel objects. OSPendMulti() can only pend on multiple semaphores and message queues, not event flags and mutual-exclusion semaphores. If the objects are already posted when OSPendMulti() is called, μC/OS-III will specify which of the objects in the list of objects have already been posted. If none of the objects are posted, OSPendMulti() will place the calling task in the pend list of all the desired objects. OSPendMulti() will return as soon as one of the objects is posted. In this case, OSPendMulti() will indicate which object was posted. OSPendMulti() is a complex function that has potentially long critical sections. 321 Chapter 16 322 Chapter 17 Memory Management An application can allocate and free dynamic memory using any ANSI C compiler’s malloc() and free() functions, respectively. However, using malloc() and free() in an embedded real-time system may be dangerous. Eventually, it might not be possible to obtain a single contiguous memory area due to fragmentation. Fragmentation is the development of a large number of separate free areas (i.e., the total free memory is fragmented into small, non-contiguous pieces). Execution time of malloc() and free() is generally nondeterministic given the algorithms used to locate a contiguous block of free memory. μC/OS-III provides an alternative to malloc() and free() by allowing an application to obtain fixed-sized memory blocks from a partition made from a contiguous memory area, as illustrated in Figure 17-1. All memory blocks are the same size, and the partition contains an integral number of blocks. Allocation and deallocation of these memory blocks is performed in constant time and is deterministic. The partition itself is typically allocated statically (as an array), but can also be allocated by using malloc() as long as it is never freed. %DVH$GGUHVV 3DUWLWLRQ %ORFN6L]H 0HPRU\%ORFN Figure 17-1 Memory Partition 323 Chapter 17 As indicated in Figure 17-2, more than one memory partition may exist in an application and each one may have a different number of memory blocks and be a different size. An application can obtain memory blocks of different sizes based upon requirements. However, a specific memory block must always be returned to the partition that it came from. This type of memory management is not subject to fragmentation except that it is possible to run out of memory blocks. It is up to the application to decide how many partitions to have and how large each memory block should be within each partition. 3DUWLWLRQ 3DUWLWLRQ 3DUWLWLRQ 3DUWLWLRQ Figure 17-2 Multiple Memory Partitions 17-1 CREATING A MEMORY PARTITION Before using a memory partition, it must be created. This allows μC/OS-III to know something about the memory partition so that it can manage their allocation and deallocation. Once created, a memory partition is as shown in Figure 17-3. Calling OSMemCreate() creates a memory partition.  26B0(0 1DPH3WU )UHH/LVW3WU    %ON6L]H 1EU0D[ 1EU)UHH 1XPEHURI%ORFNV Figure 17-3 Created Memory Partition 324 Memory Management F17-3(1) When creating a partition, the application code supplies the address of a memory partition control block (OS_MEM). Typically, this memory control block is allocated from static memory, however it can also be obtained from the heap by calling malloc(). The application code should however never deallocate it. F17-3(2) OSMemCreate() organizes the continuous memory provided into a singly linked list and stores the pointer to the beginning of the list in the OS_MEM structure. F17-3(3) Each memory block must be large enough to hold a pointer. Given the nature of the linked list, a block needs to be able to point to the next block. Listing 17-1 indicates how to create a memory partition with μC/OS-III. OS_MEM CPU_INT08U void MyPartition; MyPartitionStorage[12][100]; main (void) (1) (2) (3) { OS_ERR : err; : OSInit(&err); : OSMemCreate((OS_MEM *)&MyPartition, (CPU_CHAR *)”My Partition”, (void *)&MyPartitionStorage[0][0], (OS_MEM_QTY ) 12, (OS_MEM_SIZE)100, (OS_ERR *)&err); /* Check ’err” */ : : OSStart(&err); (4) (5) (6) (7) (8) (9) } Listing 17-1 Creating a memory partition L17-1(1) An application needs to allocate storage for a memory partition control block. This can be a static allocation as shown here or malloc() can be used in the code. However, the application code must not deallocate the memory control block. 325 Chapter 17 L17-1(2) The application also needs to allocate storage for the memory that will be split into memory blocks. This can also be a static allocation or malloc() can be used. The same reasoning applies. Do not deallocate this storage since other tasks may rely on the existence of this storage. L17-1(3) Memory partition must be created before allocating and deallocating blocks from the partition. One of the best places to create memory partitions is in main() prior to starting the multitasking process. Of course, an application can call a function from main() to do this instead of actually placing the code directly in main(). L17-1(4) Pass the address of the memory partition control block to OSMemCreate(). Never reference any of the internal members of the OS_MEM data structure. Instead, use μC/OS-III’s API. L17-1(5) Assign a name to the memory partition. There is no limit to the length of the ASCII string as μC/OS-III saves a pointer to the ASCII string in the partition control block and not the actual characters. L17-1(6) Pass the base address of the storage area reserved for the memory blocks. L17-1(7) Specify how many memory blocks are available from this memory partition. Hard coded numbers are used for the sake of the illustration but one should instead use #define constants. L17-1(8) Specify the size of each memory block in the partition. Again, a hard coded value is used for illustration, which is not recommended in real code. L17-1(9) As with most μC/OS-III services, OSMemCreate() returns an error code indicating the outcome of the service. The call is successful if “err” contains OS_ERR_NONE. Listing 17-2 shows how to create a memory partition with μC/OS-III, this time using malloc() to allocate storage. Do not deallocate the memory control block or the storage for the partition. 326 Memory Management OS_MEM void *MyPartitionPtr; (1) main (void) { OS_ERR void err; *p_storage; : OSInit(&err); : MyPartitionPtr = (OS_MEM *)malloc(sizeof(OS_MEM)); if (MyPartitionPtr != (OS_MEM *)0) { p_storage = malloc(12 * 100); if (p_storage != (void *)0) { OSMemCreate((OS_MEM *)MyPartitionPtr, (CPU_CHAR *)”My Partition”, (void *)p_storage, (OS_MEM_QTY ) 12, (OS_MEM_SIZE)100, (OS_ERR *)&err); /* Check ’err” */ } } : OSStart(&err); (2) (3) (4) (5) (6) (6) } Listing 17-2 Creating a memory partition L17-2(1) Instead of allocating static storage for the memory partition control block, assign a pointer that receives the OS_MEM allocated using malloc(). L17-2(2) The application allocates storage for the memory control block. L17-2(3) Allocate storage for the memory partition. L17-2(4) Pass a pointer to the allocated memory control block to OSMemCreate(). L17-2(5) Pass the base address of the storage used for the partition. L17-2(6) Finally, pass the number of blocks and the size of each block so that μC/OS-III creates the linked list of 12 blocks of 100 bytes each. Again, hard coded numbers are used, but these would typically be replaced by #defines. 327 Chapter 17 17-2 GETTING A MEMORY BLOCK FROM A PARTITION Application code can request a memory block from a partition by calling OSMemGet() as shown in Listing 17-3. The code assumes that the partition was already created. OS_MEM CPU_INT08U void MyPartition; *MyDataBlkPtr; (1) MyTask (void *p_arg) { OS_ERR err; : while (DEF_ON) { : MyDataBlkPtr = (CPU_INT08U *)OSMemGet((OS_MEM (OS_ERR if (err == OS_ERR_NONE) { *)&MyPartition, *)&err); (2) (3) /* You have a memory block from the partition */ } : : } } Listing 17-3 Obtaining a memory block from a partition L17-3(1) The memory partition control block must be accessible by all tasks or ISRs that will be using the partition. L17-3(2) Simply call OSMemGet() to obtain a memory block from the desired partition. A pointer to the allocated memory block is returned. This is similar to malloc(), except that the memory block comes from a pool that is guaranteed to not fragment. L17-3(3) It is important to examine the returned error code to ensure that there are free memory blocks and that the application can start putting content in the memory blocks. 328 Memory Management 17-3 RETURNING A MEMORY BLOCK TO A PARTITION The application code must return an allocated memory block back to the proper partition when finished. Do this by calling OSMemPut() as shown in Listing 17-4. The code assumes that the partition was already created. OS_MEM CPU_INT08U MyPartition; *MyDataBlkPtr; (1) void MyTask (void *p_arg) { OS_ERR err; : while (DEF_ON) { : OSMemPut((OS_MEM *)&MyPartition, (2) (void *)MyDataBlkPtr, (3) (OS_ERR *)&err); if (err == OS_ERR_NONE) { (4) /* You properly returned the memory block to the partition */ } : : } } Listing 17-4 Returning a memory block to a partition L17-4(1) The memory partition control block must be accessible by all tasks or ISRs that will be using the partition. L17-4(2) Simply call OSMemPut() to return the memory block back to the memory partition. Note that there is no check to see whether the proper memory block is being returned to the proper partition (assuming you have multiple different partitions). It is therefore important to be careful (as is necessary when designing embedded systems). L17-4(3) Pass the pointer to the data area that is allocated so that it can be returned to the pool. Note that a “void *” is assumed. L17-4(4) Examine the returned error code to ensure that the call was successful. 329 Chapter 17 17-4 USING MEMORY PARTITIONS Memory management services are enabled at compile time by setting the configuration constant OS_CFG_MEM_EN to 1 in OS_CFG.H. There are a number of operations to perform on memory partitions as summarized in Table 13-1. Function Name Operation OSMemCreate() Create a memory partition. OSMemGet() Obtain a memory block from a memory partition. OSMemPut() Return a memory block to a memory partition. Table 17-1 Memory Partition API summary OSMemCreate() can only be called from task-level code, but OSMemGet() and OSMemPut() can be called by Interrupt Service Routines (ISRs). Listing 17-4 shows an example of how to use the dynamic memory allocation feature of μC/OS-III, as well as message-passing capabilities (see Chapter 15, “Message Passing” on page 289). In this example, the task on the left reads and checks the value of analog inputs (pressures, temperatures, and voltage) and sends a message to the second task if any of the analog inputs exceed a threshold. The message sent contains information about which channel had the error, an error code, an indication of the severity of the error, and other information. Error handling in this example is centralized. Other tasks, or even ISRs, can post error messages to the error-handling task. The error-handling task could be responsible for displaying error messages on a monitor (a display), logging errors to a disk, or dispatching other tasks to take corrective action based on the error. 330 Memory Management 0HVVDJH4XHXH $QDORJ ,QSXWV  7DVN 267DVN43RVW    260HP*HW (UU0VJ3DUW  267DVN43HQG 7DVN   260HP3XW  Figure 17-4 Using a Memory Partition – non blocking F17-4(1) The analog inputs are read by the task. The task determines that one of the inputs is outside a valid range and an error message needs to be sent to the error handler. F17-4(2) The task then obtains a memory block from a memory partition so that it can place information regarding the detected error. F17-4(3) The task writes this information to the memory block. As mentioned above, the task places the analog channel that is at fault, an error code, an indication of the severity, possible solutions, and more. There is no need to store a timestamp in the message, as time stamping is a built-in feature of μC/OS-III so the receiving task will know when the message was posted. F17-4(4) Once the message is complete, it is posted to the task that will handle such error messages. Of course the receiving task needs to know how the information is placed in the message. Once the message is sent, the analog input task is no longer allowed (by convention) to access the memory block since it sent it out to be processed. 331 Chapter 17 F17-4(5) The error handler task (on the right) normally pends on the message queue. This task will not execute until a message is sent to it. F17-4(6) When a message is received, the error handler task reads the contents of the message and performs necessary action(s). As indicated, once sent, the sender will not do anything else with the message. F17-4(7) Once the error handler task is finished processing the message, it simply returns the memory block to the memory partition. The sender and receiver therefore need to know about the memory partition or, the sender can pass the address of the memory partition as part of the message and the error handler task will know where to return the memory block. Sometimes it is useful to have a task wait for a memory block in case a partition runs out of blocks. μC/OS-III does not support pending on partitions, but it is possible to support this requirement by adding a counting semaphore (see Chapter 13, “Resource Management” on page 209) to guard the memory partition. This is illustrated in Figure 17-5. 0HVVDJH4XHXH $QDORJ ,QSXWV 7DVN 267DVN43RVW (UU0VJ3DUW 7DVN   266HP3HQG 260HP*HW 267DVN43HQG 260HP3XW 266HP3RVW &RXQWLQJ 6HPDSKRUH  Figure 17-5 Using a Memory Partition - blocking 332 Memory Management F17-5(1) To obtain a memory block, simply obtain the semaphore by calling OSSemPend() and call OSMemGet() to receive the memory block. F17-5(2) To release a block, simply return the memory block by calling OSMemPut() and signal the semaphore by calling OSSemPost(). The above operations must be performed in order. Note that the user may call OSMemGet() and OSMemPut() from an ISR since these functions do not block and in fact, execute very quickly. However, you cannot use blocking calls from ISRs. 17-5 SUMMARY Do not use malloc() and free() in embedded systems since they lead to fragmentation. It is possible to use malloc() to allocate memory from the heap, but do not deallocate the memory. The application programmer can create an unlimited number of memory partitions (limited only by the amount of available RAM). Memory partition services in μC/OS-III start with the OSMem???() prefix, and the services available to the application programmer are described in Appendix A, “μC/OS-III API Reference Manual” on page 375. Memory management services are enabled at compile time by setting the configuration constant OS_CFG_MEM_EN to 1 in OS_CFG.H. OSMemGet() and OSMemPut() can be called from ISRs. 333 Chapter 17 334 Chapter 18 Porting μC/OS-III This chapter describes in general terms how to adapt μC/OS-III to different processors. Adapting μC/OS-III to a microprocessor or a microcontroller is called porting. Most of μC/OS-III is written in C for portability. However, it is still necessary to write processor-specific code in C and assembly language. μC/OS-III manipulates processor registers, which can only be done using assembly language. Porting μC/OS-III to different processors is relatively easy as μC/OS-III was designed to be portable and, since μC/OS-III is similar to μC/OS-II, the user can start from a μC/OS-II port. If there is already a port for the processor to be used, it is not necessary to read this chapter unless, of course, there is an interest in knowing how μC/OS-III processor-specific code works. μC/OS-III can run on a processor if it satisfies the following general requirements: ■ The processor has an ANSI C compiler that generates reentrant code. ■ The processor supports interrupts and can provide an interrupt that occurs at regular intervals (typically between 10 and 1000 Hz). ■ Interrupts can be disabled and enabled. ■ The processor supports a hardware stack that accommodates a fair amount of data (possibly many kilobytes). ■ The processor has instructions to save and restore the stack pointer and other CPU registers, either on the stack or in memory. ■ The processor has access to sufficient RAM for μC/OS-III’s variables and data structures as well as internal task stacks. ■ The compiler should support 64-bit data types (typically “long long”). 335 Chapter 18 Figure 18-1 shows the μC/OS-III architecture and its relationship with other software components and hardware. When using μC/OS-III in an application, the user is responsible for providing application software and μC/OS-III configuration sections. 26B&)*+ 26B&)*B$33+ $33& $33+ &38,QGHSHQGHQW /LEUDULHV 26B&)*B$33& 26B7
AD-UCOS3-SPRD 价格&库存

很抱歉,暂时无法提供与“AD-UCOS3-SPRD”相匹配的价格&库存,您可以联系我们找货

免费人工找货