Filename: HowToDevelopAnOAL.doc 1 [This topic is part of a beta release and is subject to change in future releases.] How to Develop an OEM Adaptation Layer An OEM adaptation layer (OAL) is a layer of code between the Windows CE kernel and the hardware of your target device. You develop an OAL to communicate data between your operating system (OS) and your target device and include code to handle interrupts, timers, and so on. For more information about the OAL, see OEM Adaptation Layer. Some of the startup code for your OAL may already be completed if you already implemented a boot loader. For example, you can reuse parts of the StartUp function and debug routines that were created during the boot loader development process. Conversely, if you have not already developed a boot loader, the following steps do cover implementation of stand-alone OAL startup code. For more information about developing a boot loader, see How to Develop a Boot Loader. Hardware and Software Assumptions • You developed and built your boot loader code; public headers and libraries have gone through the Sysgen phase. • You are developing the OAL without assistance from CSP OAL code. If CSP libraries and a driver exist for your hardware, use them. The libraries and driver help shorten the time during development and test phases. If you already have an existing OAL and want to migrate from a previous version of Windows CE to Windows CE .NET 4.2, see Migration Issues and How to Migrate a Board Support Package to Windows CE .NET 4.2. The set of procedures presented in following steps provides only a basic kernel with a file system and debug network support, and offers no support for drivers. To track your progress in the following table, select the check box next to each step. Note You need to create or modify other files outside of the OAL in order to create a tiny kernel board support package (BSP). For example, you need to modify Config.bib, which contains information about the RAM regions used by the OS. <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> Step Topic 1. Create a new platform directory and a subdirectory for the OAL on your development workstation. Not applicable The following example shows the basic naming convention for a platform called MyPlatform. • Platform directory: %_WINCEROOT%\Platform\MyPlatform • OAL directory: %_WINCEROOT%\Platform\MyPlatform\Kernel\Hal You need to include the directories in the dirs file if you want the directories to build as part of a platform build. For more information about dirs file, see Dirs File. Note To comply with directory naming conventions, include CPU-specific or assembly language files in a further subdirectory based on the Filename: HowToDevelopAnOAL.doc 2 CPU name. For example, if the example platform is ARM-based, then the StartUp code described in the next step would be located in %_WINCEROOT%\Platform\MyPlatform\Kernel\Hal \ARM. <IN 2. Implement the Startup function for the OAL. Implementing the OAL PUT Note In general, StartUp initializes the CPU core, StartUp Function TYP including the SDRAM controller, memory E=" management unit (MMU), and caches. The function Che performs this in preparation for running the ckb Windows CE kernel. ox" Parts of this code can be shared with your platform's Val boot loader. Be careful when initializing hardware ue= twice in this situation. OAL StartUp should function "ON whether the boot loader has run. For example, the "> OAL should not attempt to enter x86 protected mode if the boot loader has already done so. You can typically obtain this function from a sample platform using the same CPU core. <IN 3. Create a sources file and a makefile to assemble and Creating the OAL PUT compile the file containing the StartUp function. Sources and Makefile TYP Files E=" Che ckb ox" Val ue= "ON "> <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 4. Build the StartUp source file. <IN PUT TYP E=" Che ckb 5. Create the kernel Buildexe directories and create a dirs file to direct the build process. This step verifies that the build files are correct and in place. This step creates a Hal.lib binary in your hardware platform's Lib directory. The Windows CE kernel, of which the OAL is a part, is an .exe file that is created as part of the BSP build process. Building the OAL Source Code Creating the Kernel Buildexe Directory Filename: HowToDevelopAnOAL.doc ox" Val ue= "ON "> 3 There are typically three different kernel variants: basic kernel, kernel with KITL, and a profiling kernel. The focus of this OAL development process is on the kernel with KITL. You will need to create additional build directories and sources files if you want to create the remaining kernel images. Note If you try to build the Kernkitl.exe image at this stage, you will receive a number of unresolved externals since a number of required functions have not yet been implemented. <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 6. Create stub versions of the following CPU-specific OAL functions: Creating Stubs for OAL Functions For ARM-based platforms only: • OEMARMCacheMode • OEMDataAbortHandler • OEMInterruptHandler • OEMInterruptHandlerFIQ For MIPS-based platforms only: • CacheErrorHandler • CacheErrorHandler_End For x86-based platforms only: • OEMNMIHandler For SHx-based platforms only: • Not applicable Note Create the source file with these stub routines in the %_WINCEROOT%\Platform\MyPlatform\Kernel\Hal \<CPU> directory. In this step and in the following steps, you will need to create stub versions to resolve link-time dependencies, and thus, allow for an OAL and a kernel image to be built. You can then add implementation details to each of the functions in an ordered approach. The routines that follow are roughly divided into functional areas. These areas may lend themselves to being organized into different source files. Any new source files should be added to the sources file to ensure that they are compiled and included in the OAL library. <IN 7. Create stub versions of the following required OAL PUT functions: TYP • InitClock E=" Note Required by DoPowerOff but usually Che called by OEMInit. ckb Creating Stubs for OAL Functions Filename: HowToDevelopAnOAL.doc ox" • Val • ue= • "ON "> <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 4 OEMInit OEMCacheRangeFlush OEMGetExtensionDRAM 8. Create stub versions of the following functions: • OEMDebugInit • OEMInitDebugSerial • OEMReadDebugByte • OEMWriteDebugByte • OEMWriteDebugString • OEMParallelPortGetByte • OEMParallelPortSendByte Creating Stubs for OAL Functions Note The OEMParallelPortXXX functions are obsolete. You do not need to implement them, but stub versions must be created to build a kernkitl image. <IN 9. Create stub versions of the following functions: PUT • OEMIdle TYP • OEMPowerOff E=" Che ckb ox" Val ue= "ON "> Creating Stubs for OAL Functions <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 10. Create stub versions of the following interrupt functions: Creating Stubs for OAL Functions <IN PUT TYP E=" Che ckb 11. Create stub versions of the following real-time clock Creating Stubs for OAL (RTC) functions: Functions • OEMInterruptDisable • OEMInterruptDone • OEMInterruptEnable • SC_GetTickCount • OEMGetRealTime • OEMSetAlarmTime • OEMSetRealTime Filename: HowToDevelopAnOAL.doc 5 ox" Val ue= "ON "> <IN 12. Create a stub versions of the following function: PUT • OEMIoControl TYP E=" Che ckb ox" Val ue= "ON "> <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 13. Define the following global variables required by Nk.lib. Not applicable Required for all platforms: • NoPPFS Additional variable for ARM-based and x86-based platforms only: • OEMAddressTable Additional variable for MIPS-based platforms only: • OEMTLBSize Additional variables for SHx-based platforms only: • <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> Creating Stubs for OAL Functions SH3CacheLines or SH4CacheLines 14. Build the kernel executable image, Kernkitl.exe, when stub versions of all the functions and global variables are defined. Building the Kernel Executable Image Note At this point, the kernel image is not yet very useful, but if you successfully create the resulting Kernkitl.exe image, this establishes the OAL framework and verifies that the build-related files are configured properly. After creating Kernkitl.exe, you can start adding functionality to the image in stages. For the remainder of the OAL development process, each stage is implemented incrementally. However, because there is no support for the kernel debugger, you must perform debugging and verification at this point using any of the following forms of debugging: a hardware probe or debugger, serial prints, LED blinks, or another form of debugging. <IN 15. Implement the following CPU-specific Microprocessor-Specific Filename: HowToDevelopAnOAL.doc PUT TYP E=" Che ckb ox" Val ue= "ON "> pre-OEMInitDebugSerial functions and global variables: 6 Issues For ARM-based platforms only: • OEMAddressTable • OEMARMCacheMode • OEMDataAbortHandler For MIPS-based platforms only: • OEMTLBSize • CacheErrorHandler • CacheErrorHandler_End For x86-based platforms only: • OEMAddressTable • OEMNMIHandler For SHx-based platforms only: • SH3CacheLines or SH4CacheLines The code for these functions can be copied from the OAL for a platform using the same CPU, or by linking in the CSP library for your CPU. <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 16. Implement the OEMCacheRangeFlush function. Implementing the The code for this function can be copied from the OAL OEMCacheRangeFlush for a platform using the same CPU, or by linking in the Function CSP library for your CPU. <IN 17. Stop and ensure that the image builds. PUT TYP E=" Che ckb ox" Val ue= "ON "> <IN PUT TYP E=" Che 18. Create a %_WINCEROOT%\Platform\MyPlatform\Files directory and create empty files in the new directory called Platform.bib, Platform.reg, Platform.db, and Not applicable Not applicable Filename: HowToDevelopAnOAL.doc ckb ox" Val ue= "ON "> 7 Platform.dat. Copy and edit a Config.bib file from a similar platform, and then specify the MEMORY and CONFIG section information for your platform. Config.bib should minimally specify the NK and RAM section address information. Romimage.exe uses this address information to know how to organize the OS to fit the hardware resources available on your standard development board (SDB). Note The addresses in Config.bib must be virtual, not physical. ROMOFFSET should be used to ensure that the OS image downloads to the correct physical RAM or flash memory location. For more information, see the following topics: <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> • MEMORY Section • CONFIG Section • Binary Image Builder File • Romimage 19. Create a %_WINCEROOT%\Platform\MyPlatform\CESYSGEN directory and copy the makefile file from a similar platform into that directory. Not applicable For more information about makefiles, see Makefile File. <IN 20. Ensure that all appropriate platform directories have Not applicable PUT dirs files. TYP For more information, see Dirs File. E=" Che ckb ox" Val ue= "ON "> <IN PUT TYP E=" Che ckb 21.From the command line, build an Nk.bin image by entering the following command. blddemo clean -q The .bin file contains the Windows CE kernel, and depending on the build flags, it can also contain other Not applicable Filename: HowToDevelopAnOAL.doc 8 ox" executables and support files. Val For more information about the Windows CE build tool, ue= see Build Tool. "ON "> <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 22. Verify that you can download the Nk.bin image and Not applicable that you can boot up to at least the OEMInitDebugSerial function in the OAL using any of the following debugging tools: hardware debugger or probe, LED write, or serial debug prints. Put while(1); in your code to stop the boot. Note Once OEMInitDebugSerial is reached, the CPU will be executing in virtual address mode. Do not use physical addresses, for example, when writing to the debug serial port or LEDs, from this point forward. If OEMInitDebugSerial is never reached, you may have incorrectly specified the OEMAddressTable or are otherwise causing unmapped memory to be accessed, triggering an abort or exception. After completing this step, you have confirmed that the kernel has set up its virtual memory structures or tables and that caches, translation look-aside buffers (TLBs), and write buffers are enabled. <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 23. Implement the following serial debug functions. Enabling the Debug These are the same routines used by the boot loader so Serial Port you can share the code with the OAL. Implementing the Serial Debug • OEMInitDebugSerial Functions • OEMReadDebugByte <IN PUT TYP E=" Che ckb ox" Val ue= "ON 24. Rebuild Nk.bin and verify that the boot process proceeds through OEMInit. • OEMWriteDebugByte • OEMWriteDebugString Note Sharing these functions between the boot loader and OAL may require minor changes. For more information, see Sharing Code Between the Boot Loader and the OAL. You should also verify that the kernel calls OEMInitDebugSerial before it calls OEMInit and that serial text is written to the initialized UART. Not applicable Filename: HowToDevelopAnOAL.doc 9 "> <IN 25. Implement the OEMInit function. PUT TYP E=" Che ckb ox" Val ue= "ON "> <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 26. Implement the interrupt-related code. Implementing an ISR For all platforms: • OEMInterruptEnable • OEMInterruptDisable • OEMInterruptDone For ARM-based platforms only: • OEMInterruptHandler • OEMInterruptHandlerFIQ For non-ARM platforms: • An ISR that handles the 1 millisecond (ms) system tick interrupt <IN 27. Implement the power management functions: PUT • OEMIdle TYP • OEMPowerOff E=" Note Implementation of the OEMPowerOff Che function is highly device-dependent and often ckb requires changes to the boot loader to resume ox" from the suspend state. Implementing suspend Val and resume is beyond the scope of this ue= procedure. You may leave the OEMPowerOff "ON function stubbed out. "> <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> Implementing the OEMInit Function 28. Rebuild the Nk.bin image and verify that it boots successfully. Using a hardware debugger or probe, serial prints, or LEDS, verify that OEMInit returns and that OEMIdle is eventually called. Enabling Power Management Not applicable Filename: HowToDevelopAnOAL.doc <IN 29. Add KITL initialization code to OEMInit. PUT TYP E=" Che ckb ox" Val ue= "ON "> 10 Adding KITL Initialization Code <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 30. Add the Windows CE Target Control Shell to your OS Not applicable image. <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 31. Rebuild the Nk.bin image and verify that it boots successfully. Ensure that PB can connect over KITL to the target using the Windows CE Target Control. <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> At the build shell prompt, enter the following command: set SYSGEN_SHELL=1 Not applicable Verify that the gi [proc | thrd | mod | delta | all] commands work. After this step, you have the basic kernel in place. The basic kernel has interrupts enabled and uses virtual memory, caches, TLB, and write buffers. These features make up most of the tiny kernel or minkern functionality. 32. Implement the following RTC functions: • OEMGetRealTime • OEMSetAlarmTime • OEMSetRealTime Implementing the Real-Time Clock and System Timer <IN 33. Customize memory usage by doing the following Customizing Memory PUT tasks: TYP • Override MainMemoryEndAddress if you want to Filename: HowToDevelopAnOAL.doc E=" Che ckb • ox" Val ue= "ON "> <IN PUT TYP E=" Che ckb ox" Val ue= "ON "> 11 include additional contiguous memory than is described in Config.bib. Implement OEMGetExtensionDRAM if you want to include additional noncontiguous memory. 34. Implement the OEMIoControl function. The IOCTLs supported by the OEMIocontrol function are highly platform-dependent and feature-dependent. The following list shows some of the common IOCTLs you may choose to support: • IOCTL_HAL_DDK_CALL • IOCTL_HAL_GETBUSDATA • IOCTL_HAL_SETBUSDATA • IOCTL_HAL_DISABLE_WAKE • IOCTL_HAL_ENABLE_WAKE • IOCTL_HAL_GET_DEVICE_INFO • IOCTL_HAL_GET_DEVICEID • IOCTL_HAL_GET_UUID • IOCTL_HAL_GET_WAKE_SOURCE • IOCTL_HAL_INIT_RTC • IOCTL_HAL_INITREGISTRY • IOCTL_HAL_POSTINIT • IOCTL_HAL_PRESUSPEND • IOCTL_HAL_QUERY_DISPLAYSETTINGS • IOCTL_HAL_REBOOT • IOCTL_HAL_RELEASE_SYSINTR • IOCTL_HAL_REQUEST_IRQ • IOCTL_HAL_REQUEST_SYSINTR • IOCTL_HAL_SET_DEVICE_INFO • IOCTL_HAL_TRANSLATE_IRQ • IOCTL_KITL_GET_INFO • IOCTL_PROCESSOR_INFORMATION • IOCTL_SET_KERNEL_COMM_DEV • IOCTL_SET_KERNEL_DEV_PORT • IOCTL_VBRIDGE_802_3_MULTICAST_LIST • IOCTL_VBRIDGE_CURRENT_PACKET_FILTER • IOCTL_VBRIDGE_GET_ETHERNET_MAC • IOCTL_VBRIDGE_GET_RX_PACKET • IOCTL_VBRIDGE_GET_RX_PACKET_COMPLETE Implementing the OEMIoControl Function Filename: HowToDevelopAnOAL.doc • IOCTL_VBRIDGE_GET_TX_PACKET • IOCTL_VBRIDGE_GET_TX_PACKET_COMPLETE • IOCTL_VBRIDGE_SHARED_ETHERNET • IOCTL_VBRIDGE_WILD_CARD 12 See Also Completing an OAL | Testing an Enhanced OAL | How to Create a Board Support Package | How to Bring Up a Device by Creating a New BSP | Hardware Adaptation How-to Topics Filename: HowToDevelopAnOAL.doc 13 Implementing the OAL StartUp Function The source file, located in %_WINCEROOT%\Platform\<Platform>\Kernel\Buildexe\<Kernel Type>, specifies the startup function of the kernel with the assignment EXEENTRY=StartUp. The StartUp function specified is executed either by a jump from the boot loader or through the CPU reset vector. The StartUp function has the following two purposes: • It initializes the CPU to a known state. For more information about initializing the CPU, see Initializing the CPU. • It calls the kernel initialization function, which takes no parameters. For more information about initializing the kernel, see Initializing the Kernel. For sample implementations of the StartUp and OAL initialization functions, see %_WINCEROOT%\Platform\<Platform>\Kernel\Hal\<Microprocessor>. For information about the implementation details for the StartUp function during the boot loader development process, see Implementing the Boot Loader StartUp Function. See Also How to Develop an OEM Adaptation Layer | How to Develop a Boot Loader Filename: HowToDevelopAnOAL.doc 14 Initializing the CPU This is the OEM hardware initialization code where you set up ROM and DRAM access to get the minimum CPU running. For sample hardware initialization code, see x86 Kernel. When the StartUp function is called, the boot loader may already have initialized some of the hardware. OAL StartUp must be flexible enough to handle this situation. For example, it may be safe to initialize some hardware twice, while other hardware must not be reinitialized. During execution of the StartUp function, you must initialize the CPU and put the CPU into a known state before calling the kernel initialization function. The following topics detail processor-specific StartUp considerations: • ARM Kernels • MIPSII and MIPSIV Kernel • SHx Kernels • x86 Kernel To ensure that the kernel is successful in completing StartUp, include some debug support, based on the SDB support, to show the progress through StartUp such as with LEDs or serial output. See Also Initializing the Kernel | Implementing the OAL StartUp Function | How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 15 ARM Kernels Before control is transferred to the kernel, the boot loader calls the StartUp routine to put the CPU into an initialized state. You may also have to perform BSP/CPU specific initializations as mentioned in the BSP user manual. The following table shows the two ARM-based BSPs and source file locations included in Platform Builder for which the StartUp function is specified. BSP Source file ARM Integrator %_WINCEROOT%\Platform\ARMIntegrator\Kernel\Hal\A RM Intel XScale DBPXA250 %_WINCEROOT%\Platform\XSC1BD\Kernel\Hal\ARM OEMs developing for the ARM processor using the ARM kernel are expected to perform the following tasks: • Put the processor in supervisor mode. • Disable the interrupt request (IRQ) and fast interrupt request (FIQ) inputs at the CPU. • Disable the memory management unit (MMU) and both the instruction and data caches. • Flush or invalidate the instruction and data caches and the translation look-aside buffer (TLB) and empty the write buffers. • Determine the reason you are in the startup code, such as cold reset, watchdog reset, GPIO reset, and sleep reset. • Configure the GPIO lines per the requirements of the board. GPIO lines must be enabled for on-board features like LED. • Configure the memory controller, set refresh frequency, and enable clocks. Program data width and memory timing values and power up the banks. • Configure the interrupt controller. Mask and clear any pending interrupts. • Initialize the real-time clock count register to 0 and enable the real-time clock. • Set up the power management/monitoring registers. Set conditions during sleep modes. • Turn on all board-level clocks and on chip peripheral clocks. • Get the physical base address of the OEMAddressTable and store in r0. • Jump to KernelStart. To enable floating-point support, see ARM Vector Floating-Point Unit Support. OEMs developing for the XScale processor can do the following to improve performance: XScale performance can be improved by changing the Branch Target Buffer Enable bit. To enable this, have the OAL change the bit in OEMInit. The branch target buffer enable bit is bit 11 of the ARM control register. See Also Initializing the CPU | Implementing the OAL StartUp Function Filename: HowToDevelopAnOAL.doc ARM Vector Floating-Point Unit Support The ARM kernels enable support for ARM floating-point units (FPUs). The standard specification for the floating-point unit requires, at a minimum, imprecise handling of floating point exceptions where an error is not detected until a second error occurs. To fully enable support for the FPU, you must implement the functions shown in the following code example. void OEMRestoreVFPCtrlRegs(LPDWORD lpExtra, int nMaxRegs); void OEMSaveVFPCtrlRegs(LPDWORD lpExtra, int nMaxRegs); BOOL OEMHandleVFPException(EXCEPTION_RECORD *er, PCONTEXT pctx); The following functions are assigned their appropriate addresses in the OAL and then during OEMInit: • pOEMSaveVFPCtrlRegs • pOEMRestoreVFPCtrlRegs • pOEMHandleVFPException See Also ARM Kernels 16 Filename: HowToDevelopAnOAL.doc 17 MIPSII and MIPSIV Kernel Before control is transferred to the kernel, the boot loader calls the StartUp routine to put the CPU into an initialized state. OEMs may also have to perform additional SDB/CPU specific initializations as mentioned in the user manual. Platform Builder ships with two MIPS-based BSP for which the StartUp function is specified in the source file directories in the following table. BSP Source file NEC Solution Gear 2 (Rockhopper) %_WINCEROOT%\Platform\SG2_VR5500\Kernel\Hal\MI PS NEC DDB-Vr4122 (Eagle) %_WINCEROOT%\Platform\Eagle\Kernel\Hal\MIPS OEMs developing for the MIPS processor using the MIPS kernel should perform the following tasks: • Set the CPU to run in uncached mode • Clear watch registers • Setup configuration registers • Set BCU registers • Initialize and invalidate the CPU cache • Clear the HAL timer • Initialize memory controllers, if necessary After the final step, StartUp must call KernelStart to allow the kernel to initialize. The function address for KernelStart should preferably be an uncached and unmapped addressed in kernel segment 1 (kseg1). Kseg1 is an address range defined in the MIPS architecture and has the following specific access characteristics: the CPU should be in kernel mode, there are no TLB mappings, and the address range is non-cacheable. See Also Initializing the CPU | Implementing the OAL StartUp Function Filename: HowToDevelopAnOAL.doc 18 SHx Kernels Before control is transferred to the kernel, the boot loader passes control to the StartUp routine to put the CPU into as initialized state. Platform Builder ships with two SHx based BSPs for which the StartUp function is specified in the source file directories in the following table. BSP Source file Hitachi US7729 HARP (Keywest) - SH3 %_WINCEROOT%\Platform\Keywest\Kernel\Hal\SH Hitachi US9950 HARP (Aspen) - SH4 %_WINCEROOT%\Platform\Aspen\Kernel\Hal\SH OEMs developing for the SHx processor using the SHx kernel are expected to perform the following tasks: • Set up bus state control registers • Set up frequency control registers • Disable cache • Initialize SDRAM (SH3 only) • Initialize watchdog timer When the FRQ Control Register is modified it creates instability in the PLL. For this reason, when FRQCR is modified, the CPU shuts off. The watchdog timer can be programmed to cause a system wakeup with an interval of 0. Until the bus controller has been initialized, some of debug devices, such as discrete LEDs and timing loops, may not be correct. BCR1 sets the types of memory that are being used. BCR2 is used to set the bus widths of all of the memory areas. Initialize the SDRAM memory in Area 3 (SH3 specific). Load the SDRAM mode register by appending the initialization value with the lower address bits of the memory area reserved by the SH3. Because the lower two address bits are not connected to the SDRAM chips (A0 & A1), you must shift the value left by two bits. See Also Initializing the CPU | Implementing the OAL StartUp Function Filename: HowToDevelopAnOAL.doc 19 x86 Kernel When the x86 StartUp function is called, you must pass the kernel OEMAddressTable to set up the page tables and the CPU capability bits to determine the level of support passed when KernelInitialize is called. Platform Builder ships with two X86-based BSPs for which the StartUp function is specified in the following shared source directory. BSP Source file National Geode SP4SC30 %_WINCEROOT%\Public\Common\Oak\CSP\i486\OAL CEPC %_WINCEROOT%\Public\Common\Oak\CSP\i486\OAL You are expected to perform the following tasks before handing the control to the x86 kernel: • CPU state should be protected mode, paging disabled (CR0, CR3, and CR4 initialized) • Initialize memory controllers • Place CPU capability bits in EDI before jumping to KernelInitialize • Place physical address of OEMAddressTable in ESI before jumping to Kernelnitialize See Also Initializing the CPU | Implementing the OAL StartUp Function Filename: HowToDevelopAnOAL.doc 20 Initializing the Kernel The kernel initialization function is named KernelInitialize for x86-based platforms and KernelStart on all other platforms. They are the first non-OEM functions that are called after StartUp has completed it tasks. The kernel initialization function calls additional OAL functions, schedules threads, but must also complete tasks that are specific to each class of CPU. For processor specific kernel initialization considerations, see the following topics: • Initializing the ARM Kernel • Initializing the MIPS Kernel • Initializing the SHx Kernel • Initializing the x86 Kernel KernelInitialize and KernelStart call SystemInitialization, which calls the following functions: • OEMInit • OEMInitDebugSerial • OEMWriteDebugString The following table shows the naming convention for your kernel's StartUp function. Startup function Kernel StartUp ARM, MIPS, x86 See Also Other Kernel Tasks | Initializing the CPU | Implementing the OAL StartUp Function | How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc Initializing the ARM Kernel The following sequence describes how the ARM kernel is initialized by the operating system (OS): • Initialize first level page tables • Enable MMU and cache • Enable stacks for each mode • Initialize kernel global data • Perform serial debug functions • Call OEMInit • Perform memory initializations • Perform other initializations See Also Initializing the Kernel | Implementing the OAL StartUp Function 21 Filename: HowToDevelopAnOAL.doc 22 Initializing the MIPS Kernel All MIPS BSPs are required to initialize OEMTLBSize to the number of translation look-aside buffer (TLB) entries supported by the CPU before calling KernelStart. The kernel calls OEMCacheRangeFlush to flush the instruction cache and get to a known state. The OEM must implement OEMCacheRangeFlush in the OAL. The kernel also calls OEMCacheRangeFlush to flush the data cache and get to a known state. For more information about implementing OEMCacheRangeFlush, see Implementing the OEMCacheRangeFlush Function. The following sequence describes how the MIPS kernel is initialized by the operating system (OS): • Implement OEMCacheRangeFlush in the OAL • Initialize kernel global data • Perform serial debug operations • Call OEMInit • Perform memory initializations • Perform other initializations At this point, the scheduler is able to run and schedule threads. See Also Initializing the Kernel | Implementing the OAL StartUp Function Filename: HowToDevelopAnOAL.doc 23 Initializing the SHx Kernel The following sequence describes how the SHx kernel is initialized by the operating system (OS): • Initialize the frequency register • Disable the CPU cache and flush • Initialize kernel global data • Serial port initializes • Initializes address translation hardware • Call OEMInit • Perform memory initializations • Initialize the status register — Load new status register with kernel mode, exceptions enabled • Register bank 0, interrupts unmasked • Initialize the debugger sub system • Perform other initializations See Also Initializing the Kernel | Implementing the OAL StartUp Function Filename: HowToDevelopAnOAL.doc 24 Initializing the x86 Kernel The following sequence describes how the x86 kernel is initialized by the operating system (OS): • Check to see if 4 MB page sizes are supported; if not, it uses a 4 KB page size • Read OEMAddressTable • Enable paging (4 MB page size or 4 KB) • Initialize kernel global data • Perform serial debug functions • Initialize the interrupt dispatch tables • Initialize the page tables and floating units • Call OEMInit • Perform memory initializations • Perform final initializations Note Kernelstart is called KernelInitialize for x86-based platforms. See Also Initializing the Kernel | Implementing the OAL StartUp Function Filename: HowToDevelopAnOAL.doc 25 Other Kernel Tasks The following topics contain information about the other tasks performed by the Windows CE .NET kernel: • Kernel Relocation • Memory Initialization • Completing Kernel Initialization • Halting the System See Also Initializing the Kernel | Implementing the OAL StartUp Function Filename: HowToDevelopAnOAL.doc Kernel Relocation During this stage, kernel data is relocated and global data for the kernel is initialized. If using a debug build, the kernel must be part of the image, otherwise the following message is sent and the boot is halted. "ERROR: Kernel must be part of ROM image!" See Also Other Kernel Tasks | Initializing the Kernel 26 Filename: HowToDevelopAnOAL.doc 27 Memory Initialization The kernel initializes any multiple XIP regions by reading OEMRomChain. For more information about multi-XIP regions, see Multiple XIP Regions. The kernel checks RAM to determine if this is a cold boot or warm rest. If this is a cold boot, the following actions take place: • A debug string is sent. The following example code shows the debug string: "Old or invalid version stamp in kernel structures - starting clean!" • The kernel memory structures are set up to indicate a cold boot. Filesys.exe will use this information later to determine if the object store is a cold or warm boot. • Kernel calls OEMGetExtensionDRAM. The kernel calls to determine if there is a second, noncontiguous memory section available to the kernel. • If pNKEnumExtensionDRAM is assigned a function pointer then this function is called, and not OEMGetExtensionDRAM to add up to 16 memory sections. The kernel can only address 512 MB of memory. • Calculate the amount of memory that will be allocated to the object store. You can calculate the amount of memory by using the information in Config.bib called FSRAMPERCENT. • pOEMCalcFSPages is called to provide an opportunity to override this value based upon information gathered at boot time. Memory is divided between the program space and the object store and the object store can only be allocated a total of 256 MB of memory. If this is a warm boot the following events occur: • The previous memory configuration is used. • CacheSync(0). • Zeros memory. • Zero only the amount of memory specified by the OEM in dwOEMCleanPages. See Also Other Kernel Tasks | Initializing the Kernel Filename: HowToDevelopAnOAL.doc 28 Completing Kernel Initialization After the kernel has completed initializing the CPU and memory, and initialized or relocated the kernel data, the kernel performs the following steps to complete the kernel initialization process. To complete initialization a. Kernel initializes the debug subsystem. b. Kernel initializes the following OAL time functions: • KSystemTimeToFileTime • KLocalFileTimeToFileTime • KFileTimeToSystemTime • KCompareFileTime c. Kernel creates an idle thread to zero the remaining memory. This thread is an idle thread that will run after the system fully boots and comes to a steady state. d. Kernel creates the alarm thread that is used by the notification system to trigger time-based events. During execution of the alarm thread the kernel will call the following functions: • OEMGetRealTime • OEMSetAlarmTime The alarm thread relies on dwNKAlarmResolutionMSec to set the resolution of the real-time clock alarm. By default, this is set to 10 seconds but can be overridden by the OAL in OEMInit. e. Kernel calls the OEMIoControl function with IOCTL_HAL_POSTINIT. This is the last chance for an OEM to perform work before the rest of the OS is started. See Also Other Kernel Tasks | Initializing the Kernel Filename: HowToDevelopAnOAL.doc 29 Halting the System In the rare case when the system encounters a non-recoverable error and the only recourse is to halt the system, it posts the debug message Halting system. At that point, the kernel has effectively locked the system and posted the debug message about the halting condition. The kernel exports a function pointer that can be overridden by the OEM. If the default function pointer value is replaced by the address of an OEM function in the OAL, when the kernel goes to halt the OS, the OAL will be called. If the OEM does not override the function pointer, the default action is to halt the system. The kernel declares the following function pointer. extern void (*lpNKHaltSystem)(void); The following code examples show how the OEM can reassign the function pointer in the OAL. The following code example shows how to implement the OEMHaltSystem function. void OEMHaltSystem (void ) { Reset the device } The following code example shows how to implement OEMHaltSystem in OEMInit. void OEMInit() { extern void (*lpNKHaltSystem)(void); lpNKHaltSystem = OEMHaltSystem; } lpNKHaltSystem does not have any return values or take any parameters. The system will not continue to function after the lpNKHaltSystem function is called. See Also Other Kernel Tasks | Initializing the Kernel Filename: HowToDevelopAnOAL.doc 30 Creating the OAL Sources and Makefile Files The Windows CE build process is driven by makefiles with build configuration information such as CDEFINES and include and library file paths, provided by the sources file. You can use a single sources file to build the OAL into a library called Hal.lib, which will be linked into the kernel image in a later build step. For more information, see Sources File and Makefile File. To create the sources and makefile files 1. Create a sources file. The sources file must contain the following code. The code shows the macro variables for the sources file. TARGETNAME=Hal TARGETTYPE=LIBRARY RELEASETYPE=PLATFORM ARM_SOURCES=arm\startup.s For more information about sources files and .bib files, see Sources File and Binary Image Builder File. f. Create a makefile file. The makefile file must contain the following code. !INCLUDE $(_MAKEENVROOT)\makefile.def For more information about makefile files, see Makefile File. See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 31 Building the OAL Source Code When you build the OAL source code, you generate a file called Hal.lib. Hal.lib is the name specified in the TARGETNAME macro variable of the sources file created when you create the OAL sources and makefile files. For more information, see Creating the OAL Sources and Makefile Files. To build the StartUp source code 1. At the command prompt in the %_WINCEROOT%\Platform\MyPlatform\Kernel\Hal directory, enter the following command: build -c For more information about the build tool and the parameters you can use, see Build Tool. g. Verify that %_WINCEROOT%\Platform\MyPlatform\Lib\ARMV4I\Retail\Hal.lib was created. If the Hal.lib binary was not created, consult Build.log for more information. Build.log is a log file written by the build subsystem in the source directory. See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 32 Creating the Kernel Buildexe Directory The Kernel\Buildexe directory contains source code files for building and linking the kernel. To create the Kernel\Buildexe directory 1. Copy an existing platform's %_WINCEROOT%\Platform\<Platform>\Kernel\Buildexe directory to your own. Make sure dirs files are added through the directories to support build recursion into the tree. For more information about dirs files, see Dirs File. h. Edit the %_WINCEROOT%\Platform\<Platform>\Kernel\Buildexe\Dirs file and comment out the Kern and Kernkitlprof entries using !if0 and !endif tags. This will simplify your task because you are only focusing on the KITL-enabled kernel image. i. Edit the %_WINCEROOT%\Platform\<Platform>\Kernel\Buildexe\Dirs file and add the Buildexe directory. j. Edit the Kernkitl sources file and comment out all library references except for Nk.lib, Hal.lib, and FullLibc.lib. You can add the other libraries later when you need them. For more information, see Kernel Image Libraries. See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 33 Creating Stubs for OAL Functions Stubs are routines that do not contain executable code. They act as placeholders for functions that need to be fully implemented later. When implementing stubs, you can leave comments that describe what you eventually need to do to add functionality to your function. By scanning existing sample platform code, you can easily obtain the function header, such as return type and arguments, for each of these functions. You can either create the stub functions from scratch or copy an existing boot loader or OAL that is of similar CPU type and board design, and then use #if0 and #endif tags for the function contents. To create a stub function 1. Obtain the function header definition for the function to be stubbed by referring to a sample platform that supports similar hardware. The function header includes a return type and a series of function arguments. k. From the function header definition, create an empty function implementation. l. If the function header specifies a return type, add a placeholder return value to satisfy the compiler. For example, if the function returns a Boolean value, add either return (TRUE); or return (FALSE); to satisfy the compiler. A successful function call is usually signaled by a TRUE or 0 value, while failure is usually signaled by a FALSE or non-zero value. However, this depends on the function and the data being returned. The following code example shows the stub for the OEMReadData function: BOOL OEMReadData (DWORD cbData, LPBYTE pbData) { return(TRUE); } See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 34 Building the Kernel Executable Image The kernel, combined with the OAL, is the first service to run, and through the bring-up stages, it runs without the benefit of any sophisticated debug services. Bring the kernel up in stages and verify each stage before proceeding. To build the kernel executable image 1. From the command prompt, go to the kernel subdirectory: %_WINCEROOT%\Platform\<Platform>\Kernel. m. At the command prompt, enter the following command: build This generates %_WINCEROOT%\Platform\MyPlatform\Target\ARM\Kernkitl.exe. A Build.log file is also generated in the current working directory, which summarizes the build process. If the build fails, errors are recorded in the build log file. For more information about the Windows CE build tool, see Build Tool. n. Resolve any build errors by adding the necessary stub function or variable. For more information about creating stubs, see Creating Stubs for OAL Functions. See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 35 Microprocessor-specific Issues With the number of CPUs being supported by Windows CE and the variations of a simple core, installing a native application onto Windows CE has become problematic. Wceload.exe and other installer applications use the dwProcessorType member from the SYSTEM_INFO structure to determine what .cab file to install to the device. The problem is that dwProcessorType may return a number that has not been documented before the release of Wceload.exe and therefore Wceload.exe does not know what CPU-based .cab file needs to be installed. To solve this problem, the QueryInstructionSet API examines what instruction set is supported. The kernel knows explicitly what instruction set it can run for a given kernel. This enables a process to query for the level of support and, in the case of a WCELOAD query, to see if the .cab file should be installed. The following topics detail microprocessor specific considerations: • ARM Microprocessor • MIPS Microprocessor • SHx Microprocessor • x86 Microprocessor See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 36 ARM Microprocessor The following issues should be noted when using an ARM kernel: • The ARM kernel does not restrict your use of registers. In addition, you can use C code as well as assembler code when you are developing your interrupt service routines (ISRs). However, be aware that the ISR should be as small and fast as possible. • You can specify mapping of physical resources to the statically mapped (direct-mapped) virtual addresses of the kernel in 1-MB blocks. In an ARM-specific OAL, create an OEMAddressTable table that defines the mapping from the 4-GB physical address space to the kernel's 512-MB, statically mapped spaces. Each entry in the table consists of the following entries: • Virtual base address to which to map • Physical base address from which to map • Number of MB to map While the order of the values in the table is arbitrary, DRAM should be placed first for optimal performance. Because the table is zero-terminated, the last entry must be all zeroes. The kernel will create two ranges of virtual addresses from this table. One region, from 0x80000000 to 0x9FFFFFFF, will have caching and buffering enabled. The other region, from 0xA0000000 to 0xBFFFFFFF, will have caching and buffering disabled. • The OEMInterruptHandlerFIQ function must be part of the OAL for any ARM build to succeed, even if it is just a stub. • Nested interrupts. The ARM CPU architecture supports two interrupts: one hardware IRQ and one fast interrupt request. The OEM must choose a nesting scheme and perform the appropriate masking of the interrupt register in OEMInterruptHandler before re-enabling interrupts. To do so, call INTERRUPTS_ON once the lower priority interrupts have been masked. OEMInterruptHandler may call INTERRUPTS_OFF again if it requires non-reentrant code, but this call is not required because the kernel handles it when returning from OEMInterruptHandler. • When using GetSystemInfo to obtain microprocessor information, note that the dwProcessorType member of the SYSTEM_INFO structure does not return the correct microprocessor type. To have dwProcessorType return the correct microprocessor type, set the CEProcessorType global variable to PROCESSOR_ARM720 or PROCESSOR_STRONGARM, depending on your microprocessor. To obtain the microprocessor type, implement IOCTL_PROCESSOR_INFORMATION in OEMInit and assign a value to CEProcessorType. See Also How to Develop an OEM Adaptation Layer | Microprocessor-Specific Issues Filename: HowToDevelopAnOAL.doc 37 MIPS Microprocessor MIPS ISRs are limited to using a subset of the available microprocessor registers. Specifically, a MIPS ISR can only use registers AT, V0, A0, A1, A2, and A3. Use the following design principals when creating an OAL for a MIPS microprocessor: • The ISR must return by means of j ra. • Implement a system-timer ISR that increments the variable CurMSec. • OEMInit needs to register the system-timer ISR. • Create the OAL function GetTickCount to return a value from the system timer. • The OEMIdle function takes a single parameter that is only used by the MIPS CPU. This parameter is undefined for other CPUs. It is the base program status register (PSR) value and sets interrupts to be enabled. The following code example shows how OEMIdle is defined for the MIPS microprocessor. void OEMIdle(DWORD dwIdleParam); • Nested interrupts. The MIPS kernel utilizes a structure that provides the kernel with information about which nested interrupts are masked while the current interrupt is being handled. To gain access to this structure, the IntrPrio global variable must be declared in the OAL. See Also How to Develop an OEM Adaptation Layer | Microprocessor-Specific Issues Filename: HowToDevelopAnOAL.doc 38 SHx Microprocessor SH3 ISRs are limited to using a subset of the available microprocessor registers. Specifically, an SH3 ISR can only use registers r0, r1, r2, r3, and r6. Use the following design principals when creating an OAL for an SHx microprocessor: • The ISR must return by means of rts, and cannot access any addresses through the translation look-aside buffer (TLB). • Implement the nonmaskable-interrupt ISR, OEMNMI. • OEMInit must register OEMNMI. • You can define the variable OEMExtraCCR to configure the cache control register. The default value is 0. Note To place an SH4 device in write-through mode, reset the cache control register (CCR) to write-through mode in OEMInit. • The scheduler timer code is implemented in the Ktimer.c and Timer0.src files in the OAL. Sample versions of these files are located in %_WINCEROOT%\Platform\%BSP%\Kernel\Hal\Shx. • Nested interrupts. The SHx kernel utilizes a structure that provides the kernel with information about which nested interrupts are masked while the current interrupt is being handled. To gain access to this structure, the IntrPrio global variable must be declared in the OAL. See Also How to Develop an OEM Adaptation Layer | Microprocessor-Specific Issues Filename: HowToDevelopAnOAL.doc 39 x86 Microprocessor The following issues should be noted when using an x86 kernel: • The x86 kernel does not restrict your use of registers. In addition, you can use C code as well as assembler code when developing your ISRs. However, be aware that the ISR should be as small and fast as possible. • Nested interrupts. The x86 CPU architecture contains a single interrupt line and typically a PIC to support nested interrupts. To support nesting by priority, the PIC must be programmed accordingly. For more information, see the PIC documentation. When the x86 kernel intercepts an IRQ, the kernel disables interrupts, saves the registers, and then turns interrupts back on before calling the ISR. • When using GetSystemInfo to obtain microprocessor information, note that the dwProcessorType member of the SYSTEM_INFO structure does not return the correct microprocessor type. To have dwProcessorType return the correct microprocessor type, implement an I/O control code in your OAL before any other applications are run and set the CEProcessorType global variable to PROCESSOR_INTEL_486. This value cannot be set in OEMInit because the detect routine for the OS is not run until after OEMInit is called. See Also How to Develop an OEM Adaptation Layer | Microprocessor-Specific Issues Filename: HowToDevelopAnOAL.doc 40 OEMAddressTable OEMAddressTable defines a table that maps a 4 GB physical address space to the kernel's 512-MB unmapped space for a device. Each entry in the table consists of the virtual base address to map to, the physical base address to map from, and the number of MB to map. The data must be declared in a READONLY section and there must be at least one non-zero entry for the structure to be valid. For x86 platforms, the first entry must identify RAM, and it must start at 0x80000000. For ARM platforms, 0x80000000 can be mapped to anything. Each entry is of the following format. Virtual Address, Physical Address, Size. The Size must be a multiple of 4MB for x86 platforms and a multiple of 1MB for ARM. The last entry of the table must be zeroed. You can leave holes between ranges but you cannot have overlapping ranges. The only valid virtual memory mapping range is from 0x80000000 to 0x9FFFFFFF. For every entry created in the table, the kernel creates two virtual address ranges. One exists in the virtual address range from 0x80000000 to 0x9FFFFFFF and is memory that has caching and buffering enabled. The second exists in the virtual address range from 0xA0000000 to 0xBFFFFFFF and has caching and buffering disabled. Any memory that is not mapped at boot time cannot be directly accessed by an ISR without first calling CreateStaticMapping to map the address. The following code example shows an implementation of OEMAddressTable for x86 platforms, which has the restriction of VA 0x80000000 = PA 0x00000000. ARM platforms do not have this restriction. public _OEMAddressTable _OEMAddressTable: ; ; OEMAddressTable defines the mapping between Physical and Virtual Address ; o MUST be in a READONLY Section ; o First Entry MUST be RAM, mapping from 0x80000000 -> 0x00000000 ; o each entry is of the format ( VA, PA, cbSize ) ; o cbSize must be multiple of 4M ; o last entry must be (0, 0, 0) ; o must have at least one non-zero entry ; RAM 0x80000000 -> 0x00000000, size 64M dd 80000000h, 0, 04000000h ; FLASH and other memory, if any ; dd FlashVA, FlashPA, ; Last entry, all zeros dd 0 0 0 FlashSize Filename: HowToDevelopAnOAL.doc See Also ARM Microprocessor | x86 Microprocessor 41 Filename: HowToDevelopAnOAL.doc 42 Implementing the OEMCacheRangeFlush Function The Windows CE .NET kernel has replaced calls to FlushDCache, FlushICache and ClearTLB with calls to the OEMCacheRangeFlush function. OEMCacheRangeFlush has been customized for various CPU architectures and componentized into libraries in the CSP directory. These libraries must be included in order for your kernel to link correctly. The following table shows the libraries located in %_WINCEROOT%\Public\Common\Oak\CSP. CPU architecture Library MIPS Csp_mips.lib ARM\ARM720t Csp_arm720t.lib ARM\ARM920t Csp_arm920t.lib ARM\Common Csp_arm.lib ARM\SA11x0 Csp_sa11x0.lib ARM\XScale Csp_xscale.lib I486\OAL I486oal.lib SHX\SH3 Csp_sh3.lib SHX\SH4 Csp_sh4.lib Note ARM builds typically link with one or more CSP libraries. Some of the CSP routines reference global variables containing cache and TLB size information. Your platform must resolve these variables. You can further optimize such CSP implementations by hard-coding variables in a private implementation of OEMCacheRangeFlush in your platform. If you want to modify or override CSP implementations of caching code, you must put the relevant source files in the Kernel\Buildexe directory and modify each of the source files in its subdirectories to build the file. See Also Cache Flush Routines | How to Develop an OEM Adaptation Layer | How to Migrate a Board Support Package to Windows CE .NET 4.2 Filename: HowToDevelopAnOAL.doc 43 Cache Flush Routines Instruction and data caches are flushed based on the CPU architecture and the kernel. Data Cache Flush The data cache flush function OEMCacheRangeFlush is called to flush the data cache by determining how many cache lines are available and then flushing each line until all lines have been flushed. Note The old data cache flush function, FlushDCache, is deprecated and should never be used. OEMCacheRangeFlush is called in the following cases: • CacheSync is called. This is a public API that anyone can call. • When the kernel loads DLLs or executable files. • Whenever the kernel debugger is active. Instruction Cache Flush The instruction cache flush function OEMCacheRangeFlush is called at various times to flush the instruction cache by determining how many cache lines are available and then flushing each line until all lines have been flushed. Note The old instruction cache flush function, FlushICache, is deprecated and should never be used. OEMCacheRangeFlush is called in the following cases: • CacheSync is called with flags CACHE_SYNC_INSTRUCTIONS or CACHE_SYNC_DISCARD. This is a public API that anyone can call. • When DLLs or executable files are loaded or paged in by the kernel. • Whenever the kernel debugger is performing active. Note On x86-based platforms, there is only one cache flush routine called OEMFlushCache. See Also Implementing the OEMCacheRangeFlush Function Filename: HowToDevelopAnOAL.doc 44 Enabling the Debug Serial Port The kernel calls OEMInitDebugSerial after it performs basic initialization operations but before calling OEMInit. When OEMInitDebugSerial returns, the kernel calls OEMWriteDebugString and displays a logon message to your terminal program through the serial port. If the debug serial port is initialized correctly, you will see terminal output program similar to the following code example. Windows CE Kernel for MIPS Built on Mar 27 2001 at 17:58:35 In addition to these two functions, the following functions add support for the full range of functionality required by the kernel: • OEMReadDebugByte • OEMWriteDebugByte • OEMWriteDebugString • OEMClearDebugCommError These functionalities are all based in the OAL and communicate directly with your SDBs serial port. The kernel and other services that are up and running use serial debug port before KITL is initialized and the debugger is engaged. The serial port should be set to the following settings to ensure that all SDBs have a consistent approach. Setting Value Baud rate 38,400 Character size 8 bit Number of stop bits 1 Parity bit None See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 45 Sharing Code Between the Boot Loader and the OAL To minimize duplication of code for some routines used by both the boot loader and the OAL, you can share code between these two. To share code between the boot loader and the OAL 1. Create one source file called Debug.c in %_WINCEROOT%\Platform\MyPlatform\Kernel\Hal, which contains the code. o. To share the code with the boot loader, create another file called Boot_debug.c in %_WINCEROOT%\Platform\MyPlatform\Eboot. The Boot_debug.c file should contain only the following code: #include "..\kernel\hal\ARM\debug.c" Using #ifdef BOOTLOADER in the source code, you can conditionally compile the code to use physical memory addresses in the boot loader, or virtual memory addresses in the OAL. For example, assuming your peripherals are physically located at 0x40000000, and mapped to 0xB0000000 in OEMAddressTable, you would need to add 0x70000000 to the register addresses when running with virtual memory enabled. The following code example shows how to do this. void OEMInitDebugSerial(void) { #ifdef BOOTLOADER UINT32 va_periph_offset = 0x00000000; // Boot loader uses physical addresses #else UINT32 va_periph_offset = 0x70000000; // Kernel startup uses virtual addresses #endif volatile UART1reg UART1_BASE; *s2410UART1 volatile IOPreg IOP_BASE; *s2410IOP ... } See Also How to Develop an OEM Adaptation Layer = (UART1reg *) va_periph_offset + = (IOPreg *) va_periph_offset + Filename: HowToDevelopAnOAL.doc 46 Implementing the OEMInit Function The OEMInit function is responsible for initializing the board-level hardware. During OEMInit, you must initialize all hardware peripherals needed to support the platform, initiate the debug KITL transport, and set any kernel variables that are required by the kernel to enable or alter functionality. For more information about KITL initialization, see Adding KITL Initialization Code. The OEMInit function contains much of the functionality present in the OEMPlatformInit function in the boot loader. For information about implementing OEMPlatformInit, see Implementing the OEMPlatformInit Function. The following list shows the suggested initialization tasks for the OEMInit function: • Initialize interrupt mapping tables. These are two private OAL tables that map from physical interrupts, IRQs, to logical interrupts, SYSINTR values, and vice-versa. The following code example shows how to initialize the interrupt mapping tables: for (i = 0; i <= IRQ_MAXIMUM; i++) { IRQToSysIntr[i] = SYSINTR_UNDEFINED; } for (i = 0; i < SYSINTR_MAXIMUM; i++) { SysIntrToIRQ[i] = IRQ_UNDEFINED; } • Create any initial static mappings required by the OAL. For example, SYSINTR_OS_TICK may map to hardware IRQ_OS_TICK—the following code example shows how to do this. SysIntrToIRQ[SYSINTR_OS_TICK] = IRQ_OS_TICK; IRQToSysIntr[IRQ_OS_TICK] = SYSINTR_OS_TICK; For more information about Windows CE interrupts, see Interrupts and Defining an Interrupt Identifier. • Configure the system timer, real-time clock (RTC), or any other timekeeping device by implementing the InitClock function and calling it from OEMInit. For more information about initializing the system tick timer, see Initializing the System Tick Timer. • Configure any CPU-level and board-level interrupt controllers. • Provide LED debug support. While optional, it is helpful when debugging the kernel. • If necessary for your CPU type, call the HookInterrupt function to register one or more ISRs. The HookInterrupt function associates an ISR with an interrupt request line (IRQ) value. OEMInit must register the ISR for the system tick. This is the base functionality required by the kernel to schedule threads. For more information about the system tick, see Implementing the System Tick. Note HookInterrupt is not required or possible for ARM-based platforms. There are only two interrupts and they are handled by OEMInterruptHandler and OEMInterruptHandlerFIQ. Filename: HowToDevelopAnOAL.doc 47 The following code example shows how you can register the interval-timer ISR, TimerISR, for hardware interrupt 5. void OEMInit(void) { ... HookInterrupt(5, TimerISR); // Hook timer interrupt ... } • Mask all unconfigured interrupt sources at the CPU-level and board-level. This prevents interrupts from accidentally being delivered during the kernel initialization. • Leave the 1 millisecond (ms) system tick unmasked, so that timekeeping and thread scheduling will function properly. The following list shows optional features that you can implement in the OEMInit function: • Logging functions • Registry functions • Secure loader functions • Save and restore coprocessor registers • High performance counters • System halt • Event tracking • Detection of idle time • User notification alarm • Multiple execute in place (XIP) regions • Default thread quantum • Enable debug IO control • Erasing the object store • Supporting CPU utilization functionality • Object store cold boot • ARM FPU support • Zeroing memory See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 48 Initializing the System Tick Timer The system tick timer should be initialized in OEMInit with a call to InitClock, also defined in the OAL. The following list shows the suggested tasks for the InitClock function: • Decide what timer interrupt source to use for the system tick. The timer interrupt source will depend on the CPU and available external sources. • Initiate the 1 millisecond (ms) tick interval required by the kernel by configuring the timer and unmasking the interrupt. If the target device contains high-resolution timers OEMInit should also set the pQueryPerformanceCounter and pQueryPerformanceFrequency global variables. For more information about high-resolution timers, see Supporting High-Resolution Timers. See Also How to Develop an OEM Adaptation Layer | Implementing the OEMInit Function Filename: HowToDevelopAnOAL.doc 49 Implementing the System Tick The system tick is the only interrupt required by the kernel and you must hook the interrupt with HookInterrupt and then provide a system tick ISR to handle the tick. When threads are active in the system the system tick timer goes off every millisecond. The system tick ISR updates CurMSec every millisecond so blocking operations and GetTickCount calls are as accurate as possible. The tick ISR only returns SYSINTR_RESCHED to the kernel's interrupt dispatcher if a thread scheduling event is ready to occur; that is, if dwReschedTime is less than or equal to CurMSec. If no scheduling event is ready, the ISR returns SYSINTR_NOP. SYSINTR_NOP indicates that the interrupted thread will be resumed immediately, without having to wait for the kernel to run its scheduling algorithm. The system tick ISR does not schedule an interrupt service thread (IST). It returns the appropriate SYSINTR_* value, which causes the kernel to schedule an IST as needed. You must implement the system tick timer as part of the OAL. Note On ARM-based processors, you do not need to call HookInterrupt. All interrupts are serviced by the OEMInterruptHandler function. Testing Tick Support To test that the system tick has been initialized and functional, you can write to a LED when a system tick occurs. See Also How to Develop an OEM Adaptation Layer | Implementing the OEMInit Function Filename: HowToDevelopAnOAL.doc 50 Implementing an ISR An interrupt service routine (ISR) is code that handles interrupt requests (IRQs) on your target device. The ISR is the core of the OAL and is responsible for locating an interrupt source, masking it, and returning a unique identifier to the Windows CE kernel to indicate which driver needs to be used to handle the event. The ISR only needs to focus on the tick timer interrupt until other user peripherals such as keyboard, mouse, PCMCIA, and USB, are added. When support for other interrupt sources are added to the ISR, there are other interrupt support routines which will need to be implemented for mapping hardware interrupts to unique identifiers—IRQ->SYSINTR mappings, enabling and disabling interrupts, and so on. Using your OAL code, Windows CE associates each IRQ with an ISR. When an interrupt occurs, the kernel's exception handler calls the registered ISR for that interrupt. Microsoft has optimized the kernel for each microprocessor and defines constants that specify the number of IRQ lines available for each microprocessor. Using HookInterrupt in the OAL, you can register only one ISR for each IRQ line. However, you can associate an ISR with one or more interrupt identifiers. In addition, each ISR has the following characteristics: • Performs minimal interrupt processing that is enough to verify and acknowledge the hardware that is asserting the interrupt, leaving most of the processing to be performed by the interrupt service thread (IST). • Returns an interrupt identifier to the kernel when finished. This causes the kernel to schedule the appropriate IST later. Note The Windows CE exception handler only saves and restores a few registers depending on the microprocessor being used. The following table shows the registers you can use. Microprocessor Registers ARM All registers are available for use. MIPS Registers v0, AT, and a0-a3 are available for use. SHx Registers r0-r3 and r6 are available for use. x86 All registers are available for use. A device driver or the OAL can associate the interrupt identifier with an event by creating an IST and calling the InterruptInitialize function. The IST then waits for the event. When the ISR returns to the kernel, the kernel examines the returned interrupt identifier and sets the associated event. The interrupt service handler of the kernel schedules execution of the IST that the driver or OAL created. The IST calls functions in the platform-dependent driver layer to read from and write to the target device. To manage ISRs, you must implement the ISR management functions that allow the kernel to begin, service, and complete interrupt processing. The following table shows the functions you must implement to support ISR management. Function Description OEMInterruptDisable Disables a specified hardware interrupt. OEMInterruptDone Signals completion of interrupt processing. Filename: HowToDevelopAnOAL.doc 51 OEMInterruptEnable Performs any hardware operations necessary to enable a specified hardware interrupt. A device driver calls the InterruptInitialize, InterruptDisable, and InterruptDone functions to enable or disable interrupts or to signal the kernel that processing is complete for an interrupt. The kernel translates these calls into the OEMInterruptEnable, OEMInterruptDisable, and OEMInterruptDone functions, respectively. You can use the information that the kernel passes to OEMInterruptEnable to initialize an interrupt or to allocate scratch space for the vendor-defined firmware ISR. When a driver calls InterruptInitialize, the kernel will then call OEMInterruptEnable. ISR management functions typically use switch statements to decode the interrupt identifier and act on the associated hardware interrupt. The following code example shows how OEMInterruptEnable checks for each of the supported interrupt identifiers. BOOL OEMInterruptEnable (DWORD idInt, LPVOID pvData, DWORD cbData) { BOOL bRet = TRUE; // Assume successful return value. switch (idInt) { case SYSINTR_TOUCH: // Enable touch screen analog-to-digital. converter (ADC) interrupt break; ... case SYSINTR_PCMCIA_EDGE: // Enable pcmcia edge interrupt and clear it, if one is pending break; ... default: bRet = FALSE; // Return FALSE for an invalid interrupt. } return bRet; } For more information about interrupts, see the following topics: • Defining an Interrupt Identifier • Creating an Interrupt Identifier • Initializing an Interrupt • Handling an Interrupt • Disabling an Interrupt • Passing Data between an ISR and an IST Filename: HowToDevelopAnOAL.doc See Also How to Develop an OEM Adaptation Layer | Driver Development 52 Filename: HowToDevelopAnOAL.doc 53 Defining an Interrupt Identifier An interrupt identifier is a unique value used by the kernel to identify a target device that raises an interrupt that requires processing. The kernel then uses the interrupt identifier to indicate whether all handling is complete, or whether to launch an IST that handles further processing by the device driver. Platform Builder provides a set of predefined interrupt identifiers, or you can create your own. Windows CE defines a set of interrupt identifiers in the Nkintr.h file. The following table shows the special-purpose interrupts used by the OS. Interrupt identifier Description SYSINTR_NOP Indicates that the kernel should complete processing of the exception handler without setting an event. SYSINTR_RESCHED Indicates that the kernel should execute a reschedule. SYSINTR_DEVICES Specifies the base value for device class identifiers not defined by your OAL. SYSINTR_MAX_DEVICES Specifies the maximum number of allowable device interrupt identifiers. The default is 32. SYSINTR_PROFILE Used by the system for profiling. It is defined relative to the value SYSINTR_DEVICES. SYSINTR_TIMING Used by the system for latency analysis. It is defined relative to the value SYSINTR_DEVICES. SYSINTR_RTC_ALARM Indicates a real-time clock alarm. It is defined relative to the value SYSINTR_DEVICES. SYSINTR_FIRMWARE Specifies the base value for custom, OAL-defined interrupt identifiers. It is defined relative to SYSINTR_DEVICES. All OEM identifiers should be equal to, or greater than, this identifier. SYSINTR_MAX_DEVICES Specifies the maximum number of allowable device interrupt identifiers. The default is 32. SYSINTR_MAXIMUM Specifies the maximum value for interrupt identifiers, which is defined as SYSINTR_DEVICES + SYSINTR_MAX_DEVICES. All interrupt identifiers must be less than, or equal to, this value. See Also How to Develop an OEM Adaptation Layer | Implementing an ISR Filename: HowToDevelopAnOAL.doc 54 Creating an Interrupt Identifier In addition to using the predefined interrupt identifiers, you can define identifiers in your OAL for your custom target devices. Create a file named Oalintr.h, and define your non-kernel interrupt identifiers in Oalintr.h relative to the value SYSINTR_FIRMWARE. For example, you could define the value of an interrupt identifier as SYSINTR_FIRMWARE+1. The maximum value of an interrupt identifier should be less than SYSINTR_MAXUMUM or SYSINTR_FIRMWARE+23. The interrupt identifiers reside in the Oalintr.h file in %_WINCEROOT%\Platform\%BSP%\Inc and those for a CEPC reside in the Oalintr.h file in %_WINCEROOT%\Platform\CEPC\Inc. The following table shows the interrupt identifiers that are defined in Oalintr.h. Interrupt identifier Description SYSINTR_ADC Indicates an analog-to-digital converter interrupt. SYSINTR_AUDIO Indicates an audio interrupt. SYSINTR_IR Indicates an infrared interrupt. SYSINTR_ETHER Indicates an Ethernet debugging adapter interrupt. SYSINTR_KEYBOARD Indicates a keyboard interrupt. SYSINTR_PCMCIA_EDGE Indicates a PCMCIA edge transition interrupt. SYSINTR_PCMCIA_LEVEL Indicates a PCMCIA level interrupt. SYSINTR_PCMCIA_STATE Indicates a PCMCIA state interrupt. SYSINTR_SERIAL Indicates a serial port interrupt. SYSINTR_TOUCH Indicates a touch screen interrupt. SYSINTR_TOUCH_CHANGED Indicates that the touch screen data has changed. Note The kernel reserves all values below SYSINTR_FIRMWARE for future use. If you redefine the predefined interrupt identifiers, you risk breaking the model device driver (MDD) code in the platform-independent layer for native device drivers. See Also How to Develop an OEM Adaptation Layer | Implementing an ISR Filename: HowToDevelopAnOAL.doc 55 Initializing an Interrupt Windows CE initializes an interrupt either in OEMInit or as part of device driver initialization. Part of the interrupt initialization can also include the creation of an IST. The kernel begins the interrupt initialization process by calling OEMInit. Use OEMInit to register an ISR by calling the HookInterrupt function. HookInterrupt associates an ISR with a hardware IRQ. Further, you must use OEMInit to register the interrupts for the system timer because subsequent code, such as the scheduler, relies on a system timer. Some device drivers may require handling that cannot be completed in an ISR. When Windows CE calls the initialization function for these types of device drivers, you can use InterruptInitialize to register an event handle with the interrupt identifier returned by the ISR of the target device. The initialization function of the device driver can then set up an IST for the interrupt by calling the CreateThread function. The main loop of the thread should wait on the event by using the WaitForSingleObject function. Do not use WaitForMultipleObjects. The main loop should also process the interrupt. If you create a thread in the driver initialization routine, you should set the appropriate priority level of the thread with the CeSetThreadPriority function. This provides the appropriate scheduling times. When the IST has a higher priority than the interrupted thread, Windows CE interrupts the thread and runs the IST immediately. The thread priorities can range from 0 through 255, with 0 as the highest priority. The choice of the IST priority level is dependent upon the environment in which the IST needs to run. These environmental factors include the number of threads, relative priority of threads, and the amount of interrupt handling. The following code example shows the functions that a device driver initialization routine typically calls to create an event and a thread associated with an ISR. // Create an event and a thread, and then associate them. hEvent = CreateEvent(...) // Get the event handle, register the interrupt, and then // associate it with the event. InterruptInitialize(SYSINTR_SERIAL, hEvent, ...) // Create the thread for the IST, passing it the event handle. // The IST code calls WaitOnSingleObject, so give it the event handle. hThd = CreateThread(..., MyISTRoutine, hEvent, ...) CeSetThreadPriority(hThd, 152); See Also How to Develop an OEM Adaptation Layer | Implementing an ISR Filename: HowToDevelopAnOAL.doc 56 Handling an Interrupt The main technique for handling an interrupt is to associate an event to a specified ISR. Windows CE then schedules your IST when the event is triggered. A typical interrupt handling sequence includes the following steps: 1. The hardware raises an interrupt. p. The kernel looks up the IRQ of the interrupt, calls the registered ISR, and disables all lower-priority interrupts. q. The ISR performs any necessary handling, and then returns an interrupt identifier. r. If the interrupt identifier value returned by the ISR is SYSINTR_NOP, the kernel completes processing without setting an event — all interrupts are enabled — and continues performing any tasks in process before the interrupt occurred. Otherwise, the kernel sets the event you have associated with the interrupt identifier. When the ISR returns, the kernel enables all other interrupts except the one that is being processed. s. The kernel schedules the IST indicated by the interrupt identifier to run. The IST can use driver resources to get or send data and control codes to the hardware and to acknowledge the hardware interrupt. t. After completely servicing the interrupt, the IST calls the InterruptDone function. In turn, InterruptDone calls the OEMInterruptDone OAL interface function to perform any hardware actions necessary to enable the next interrupt of the target device. The following code example shows the functions that an IST typically calls. // CreateThread can supply one 32-bit parameter, so pass the event handle. // Associate this thread with the registered event. MyISTRoutine(HANDLE hEvent) { while (1) { WaitForSingleObject(hEvent, INFINITE); // Check for thread exit signal and exit if set // Interrupt processing here. // On completion, call InterruptDone. InterruptDone(InterruptId); // Loop and wait for the kernel to set the event. } } See Also How to Develop an OEM Adaptation Layer | Implementing an ISR Filename: HowToDevelopAnOAL.doc 57 Disabling an Interrupt If your OS needs to disable an interrupt, this typically occurs after the kernel has received a request to unload a driver. To disable an interrupt • Implement the OEMInterruptDisable function, which is called by the kernel when such a request has been made To disable a driver interrupt, Windows CE initiates the following sequence of steps: 1. The kernel calls the FreeLibrary function to close or unload the device driver. u. The kernel calls the DLL main entry function of the driver with the DLL_PROCESS_DETACH flag passed in as a parameter. v. The device driver calls the InterruptDisable function to disable the logical interrupt. w. InterruptDisable calls OEMInterruptDisable to disable the physical interrupt. If the IRQ line is not multiplexed, the ISR can physically disable the IRQ line associated with the interrupt identifier. If the IRQ line is multiplexed, the ISR sends an SYSINTR_NOP interrupt identifier to the kernel for interrupts with the disabled interrupt identifier to keep track of those multiplexed interrupts that have been disabled. See Also How to Develop an OEM Adaptation Layer | Implementing an ISR Filename: HowToDevelopAnOAL.doc 58 Passing Data between an ISR and an IST There may be times when you want to pass information between an ISR and an IST. For example, because calling an IST each time an IRQ arrives is a time-consuming process, you might want to design an ISR to buffer IRQ data before calling the IST. The ISR would return SYSINTR_NOP until the buffer was full, and then would return the appropriate SYSINTR identifier when the ISR is ready for the IST to run. Once the IST runs, it can pick up the data that the ISR has been buffering. To pass data between an ISR and an IST 1. Reserve physical memory for the ISR in your Config.bib file. Config.bib contains several examples of reserving physical memory for the serial and debug drivers. x. Use the reserved memory in your ISR call. Because the ISR runs in kernel mode, the ISR can access the reserved memory to buffer data. y. Call the MmMapIoSpace function in your IST to map the physical memory to a virtual address. MmMapIoSpace calls the VirtualAlloc and VirtualCopy functions to map the physical memory to a virtual-memory address that the IST can access. You can also call VirtualAlloc and VirtualCopy directly. For example, you can allocate memory outside of the virtual memory space of a process by calling VirtualAlloc with its parameters set to the following values: • dwSize >= 2 MB • flAllocationType set to MEM_RESERVE • flProtect set to PAGE_NOACCESS In Windows CE .NET, an installable ISR can easily share data with an IST as the memory can be dynamically allocated instead of being reserved in the Config.bib file. See Also How to Develop an OEM Adaptation Layer | Implementing an ISR Filename: HowToDevelopAnOAL.doc 59 Enabling Power Management Power management functions respond to system calls for turning the system off or for idling it. These system calls may be triggered by either hardware or software events, such as throwing a power switch or an idle timer count. The following table shows the functions you must implement to enable power management on your target device. Function Description OEMPowerOff Places the target device in a power-off state. OEMIdle Places the target device in an idle state. This is the lowest energy usage state possible balanced with the need to return from idle quickly. The kernel calls OEMIdle whenever there are no threads ready to run. The kernel provides an extern DWORD dwReschedTime value that indicates when the next known event is to occur.Asynchronous events such as user-generated interrupt from a keyboard or mouse can occur before then and needs to be handled. The kernel calls OEMPowerOff when the user presses the OFF button or when the system requests the device to power off. OEMPowerOff is responsible for handling the state of the board-level logic through the suspend and resume process. Note Individual drivers are responsible for handling the board-level logic for individual devices, such as video, audio, and USB. You can find code samples of OEMPowerOff and OEMIdle in %_WINCEROOT%\Platform\<Platform>\Kernel\Hal. When the kernel calls the OEMIdle function, the OEM device is requested to go into a sleep, or idle, state. This consists of saving the current state, placing the memory into a refresh state if necessary, stopping the clock, and suspending execution. In order to conserve power while continually awakening the target device, OEMIdle should sleep for as long as possible. This is usually until the sooner of two events: dwReschedTime, or the maximum delay supported by the hardware timer. When an interrupt occurs, scheduled or otherwise, the device ends its idle state, the previous state is restored, and the scheduler is invoked. If no new threads are ready to run, the kernel will again call OEMIdle. When the system returns from idle, OEMIdle must update the CurMSec variable with the real number of milliseconds that have elapsed. The sample platforms also keep partial millisecond counts, dwPartialCurMSec, in case another interrupt occurs, which will cause the system to stop idling before the system timer fires. The following code example shows how to implement OEMIdle with these variables. static DWORD dwPartialCurMSec = 0; sub-millisecond leftover. void OEMIdle( DWORD dwIdleParam ) // Keep CPU-specific Filename: HowToDevelopAnOAL.doc 60 { DWORD dwIdleMSec; DWORD dwPrevMSec = *pCurMSec; // Use for 64-bit math ULARGE_INTEGER currIdle = { curridlelow, curridlehigh }; if ((int) (dwIdleMSec = dwReschedTime - dwPrevMSec) <= 0) { // already time to wakeup return; } // just idle till tick if profiling or running iltiming if (bProfileTimerRunning || fIntrTime) { // fIntrTime : Interrupt Latency timeing. // idle till end of 'tick' CPUEnterIdle(dwIdleParam); // Update global idle time and return currIdle.QuadPart += RESCHED_PERIOD; curridlelow = currIdle.LowPart; curridlehigh = currIdle.HighPart; return; } // // Since OEMIdle( ) is being called in the middle of a normal reschedule // period, CurMSec, dwPartialCurMSec, and CurTicks need to be updated accordingly. // Once we reach this point, we must re-program the timer (if we ever did) Filename: HowToDevelopAnOAL.doc 61 // because dwPartialCurMSec will be modified in the next function call. // CPUGetSysTimerCountElapsed(RESCHED_PERIOD, pCurMSec, &dwPartialCurMSec, pCurTicks); if ((int) (dwIdleMSec -= *pCurMSec - dwPrevMSec) > 0) { dwPrevMSec = *pCurMSec; // // The system timer may not be capable of arbitrary timeouts. Get the // CPU-specific highest possible timeout available. // dwIdleMSec = CPUGetSysTimerCountMax(dwIdleMSec); // // Set the timer to wake up much later than usual, if needed. // CPUSetSysTimerCount(dwIdleMSec); CPUClearSysTimerIRQ( ); // // Enable wakeup on any interrupt, then go to sleep. // CPUEnterIdle(dwIdleParam); INTERRUPTS_OFF( ); // // We're awake! The wake-up ISR (or any other ISR) has already run. // if (dwPrevMSec != *pCurMSec) { // // We completed the full period we asked to sleep. Update the counters. // Filename: HowToDevelopAnOAL.doc 62 *pCurMSec += (dwIdleMSec - RESCHED_PERIOD); // Subtract resched period, because ISR also incremented. CurTicks.QuadPart += (dwIdleMSec - RESCHED_PERIOD) * dwReschedIncrement; currIdle.QuadPart += dwIdleMSec; } else { // // Some other interrupt woke us up before the full idle period was // complete. Determine how much time has elapsed. // currIdle.QuadPart += CPUGetSysTimerCountElapsed(dwIdleMSec, pCurMSec, &dwPartialCurMSec, pCurTicks); } } // Re-arm counters CPUSetSysTimerCount(RESCHED_PERIOD); CPUClearSysTimerIRQ( ); // Update global idle time curridlelow = currIdle.LowPart; curridlehigh = currIdle.HighPart; return; } Use the above code example and the system tick ISR to implement the extended idle period. The following code example, in the system tick ISR, is used to determine whether the kernel should schedule a thread (SYSINTR_RESCHED returned) or do nothing (SYSINTR_NOP returned). ULONG ulRet = SYSINTR_NOP; if ((int)(CurMSec >= dwReschedTime)) ulRet = SYSINTR_RESCHED; By returning SYSTINTR_NOP when appropriate, a system tick ISR can prevent the kernel from rescheduling unnecessarily and therefore save power. For more information about the variables the OAL needs to update in order to support OEMIdle, see CPU Utilization. Filename: HowToDevelopAnOAL.doc See Also How to Develop an OEM Adaptation Layer 63 Filename: HowToDevelopAnOAL.doc 64 CPU Utilization You can calculate CPU utilization for the operating system (OS) by determining how much time the OS spends in the IDLE state over a period of time. The device goes idle when the kernel calls OEMIdle. The OAL must update the following three global variables to support OEMIdle: • curridlehigh • curridlelow • idleconv Idleconv is the conversion to be applied to the 64-bit time value stored in curridlehigh and curridlelow to return a millisecond value. If curridlehigh and curridlelow are based on milliseconds then idleconv is set to 1. The values of curridlehigh and curridlelow are always increasing and the increase is based on the amount of time that the device will idle or on the amount of time that the device idled before being wakened. From an application, a call to GetIdleTime will return the OAL values. See Also Interactions between OEMIdle and the Thread Timer | Sample Platform Implementations of OEMIdle | Enabling Power Management Filename: HowToDevelopAnOAL.doc 65 Interactions between OEMIdle and the Thread Timer The OEMIdle algorithm is complicated by the fact that it must cooperate with the system tick ISR in updating CurMSec. When an interrupt wakes the CPU from its low power mode during OEMIdle, the ISR for the interrupt executes. Once it has executed, control returns to the point in OEMIdle at which the CPU was put to sleep. If a timer interrupt woke the system, the tick ISR has already updated CurMSec by one millisecond before control returns to OEMIdle. If another interrupt woke the CPU, CurMSec has not been incremented. It is up to OEMIdle to make sure that CurMSec is properly updated regardless of the wakeup interrupt. The following factors affect both OEMIdle and the system tick ISR: • Interrupt latency timing support code • Monte Carlo profiling support code • Possible support for software-only alarms See Also CPU Utilization Filename: HowToDevelopAnOAL.doc 66 Sample Platform Implementations of OEMIdle The sample platforms provided with Platform Builder include an implementation of OEMIdle that is generic in regards to the platform's timer and power management capabilities. The sample implementations abstract their interface to the platform hardware in a manner similar to the MDD/PDD division in some device drivers. You are still responsible for implementing OEMIdle but can choose to leverage the sample implementation if it makes the job easier. See Also CPU Utilization | Types of System Tick Timers | Boilerplate Interface Routines Filename: HowToDevelopAnOAL.doc 67 Types of System Tick Timers In general, platforms provide one of the following types of timers: • Fixed interval timers that cannot be reprogrammed. These interrupt the system at a fixed rate and cannot be updated once they are started. CEPC uses this kind of timer. Fixed interval timers should be avoided in mobile battery-powered systems as there is no way for the kernel to conserve power with OEMIdle. • Programmable interval timers (PITs) automatically count down to zero, generate an interrupt, and automatically reload and start over. Most SHx platforms use this kind of timer. • Free-running counters with value and compare registers count up or down until the value register equals the match register, at which point they generate an interrupt. Even after the interrupt, the value register keeps counting. To generate another interrupt, the match register must be reprogrammed. XScale and DDB5476 platforms use this kind of timer. Fixed-rate timers limit the functionality of OEMIdle and the thread timer to conserve power. For more information, see Boilerplate Interface Routines. See Also Sample Platform Implementations of OEMIdle Filename: HowToDevelopAnOAL.doc 68 Boilerplate Interface Routines The sample platforms provided with Platform Builder include generic implementations of the following functions: • QueryPerformanceCounter • QueryPerformanceFrequency • OEMIdle • SC_GetTickCount Note call. The kernel calls SC_GetTickCount to service the GetTickCount system See Also Sample Platform Implementations of OEMIdle | CPUClearSysTimerIRQ Routine | CPUEnterIdle Routine | CPUGetSysTimerCountElapsed Routine | CPUGetSysTimerCountMax Routine | CPUSetSysTimerCount Routine Filename: HowToDevelopAnOAL.doc 69 CPUEnterIdle Routine CPUEnterIdle enables interrupts and puts the CPU, or the platform, into a low-power state. When an interrupt occurs it will awaken the system. CPUEnterIdle is invoked with the same dwIdleParam parameter as OEMIdle. This routine is prototyped as the following code example indicates. void CPUEnterIdle(DWORD dwIdleParam); If your platform does not support low-power modes, the implementation can be used as shown in the following code example. void CPUEnterIdle(DWORD dwIdleParam); { fInterruptFlag = FALSE; INTERRUPTS_ON(); while (!fInterruptFlag) { // Just wait here. Any interrupt will bump us out. } } Note This requires that all ISRs set the variable fInterruptFlag, which must be declared as volatile. See Also Boilerplate Interface Routines Filename: HowToDevelopAnOAL.doc 70 CPUGetSysTimerCountMax Routine OEMIdle calls CPUGetSysTimerCountMax to determine whether it can program the timer ISR for the full interval until the next thread-scheduling event. If the requested delay, indicated by dwIdleMSecRequested, is longer than the underlying hardware can support, CPUGetSysTimerCountMax returns the maximum delay possible. This routine is prototyped as shown in the following code example. DWORD CPUGetSysTimerCountMax(DWORD dwIdleMSecRequested); The following code example requires that you define IDLE_MAX_MS with the maximum millisecond delay that the hardware supports. DWORD CPUGetSysTimerCountMax(DWORD dwIdleMSecRequested); { if (dwIdleMSecRequested > IDLE_MAX_MS) { // Our timer cannot idle more than IDLE_MAX_MS milliseconds. // The sleep time needs to be broken into reasonable chunks. return IDLE_MAX_MS; } return dwIdleMSecRequested; } See Also Boilerplate Interface Routines Filename: HowToDevelopAnOAL.doc 71 CPUSetSysTimerCount Routine This routine is prototyped as shown in the following code example. void CPUSetSysTimerCount(DWORD dwIdleMSec); The dwIdleMSec parameter is the millisecond delay until the next timer interrupt. This delay value is calculated with the help of CPUGetSysTimerCountMax so it is guaranteed to be supported by the timer hardware. This routine should program the timer hardware to generate a periodic interrupt with this interval. See Also Boilerplate Interface Routines Filename: HowToDevelopAnOAL.doc 72 CPUClearSysTimerIRQ Routine This routine is prototyped as shown in the following code example. BOOL CPUClearSysTimerIRQ(void); It takes no parameters but clears the timer interrupt and returns TRUE if the timer interrupt is pending. If the timer interrupt is not pending, it returns FALSE. Some parts of OEMIdle execute with interrupts disabled and it is possible that the timer will try to interrupt the system during those parts of the code. Platforms that implement PITs need to know whether the interrupt is pending when they update CurMSec. See Also Boilerplate Interface Routines Filename: HowToDevelopAnOAL.doc 73 CPUGetSysTimerCountElapsed Routine OEMIdle calls CPUGetSysTimerCountElapsed to determine how much time has elapsed since the last timer tick. In some situations, CPUGetSysTimerCountElapsed is called with interrupts off, making it possible for a timer interrupt to be pending when it is called. This routine must compensate for the ISR, which will also update CurMSec when its interrupt is unmasked. Systems that use a PIT should note the return value from CPUClearSysTimerIRQ as it indicates whether the PIT interrupt was serviced in time to avoid losing timer information. This routine is prototyped as shown in the following code example. DWORD CPUGetSysTimerCountElapsed( DWORD dwTimerCountdownMSec, volatile DWORD *pCurMSec, DWORD *pPartialCurMSec, volatile ULARGE_INTEGER *pCurTicks ); The following table shows the parameters used in this routine: Parameter Description dwCountdownMSec The number of milliseconds that the system expected to sleep before the next timer interrupt. pCurMSec Points to a millisecond counter, often but not always the global variable CurMSec. pPartialCurMSec Some number of timer counts, not timer ticks, not accounted for in the current value of *pCurMSec. pCurTicks Pointer to a 64-bit tick counter. CPUGetSysTimerCountElapsed determines the number of counter increments or decrements that have elapsed and adds it to *pPartialCurMSec. It then calculates the number of whole milliseconds the sum represents and updates *pCurMSec with that value. Finally, it updates *pPartialCurMSec with any counts left over from this calculation. CPUGetSysTimerCountElapsed treats *pCurTicks as a running number of timer counts, reflecting the number of times the counter has been incremented or decremented, not the number of interrupts it has generated or the number of milliseconds elapsed. The following code example shows an implementation of CPUGetSysTimerCountElapsed for an ARM platform, using a PIT. DWORD CPUGetSysTimerCountElapsed( DWORD dwTimerCountdownMSec, volatile DWORD *pCurMSec, DWORD *pPartialCurMSec, volatile ULARGE_INTEGER *pCurTicks Filename: HowToDevelopAnOAL.doc 74 volatile ULARGE_INTEGER *pCurTicks ); { TimerStruct_t *pTimer = gSysTimers[OS_TIMER].pt; DWORD dwTick = dwTimerCountdownMSec * OEMCount1ms; DWORD dwCount; // If timer IRQ pending, a full resched period elapsed if (CPUClearSysTimerIRQ( )) { *pCurMSec += dwTimerCountdownMSec; pCurTicks->QuadPart += dwTick; return dwTimerCountdownMSec; } // No timer IRQ pending, calculate how much time has elapsed dwCount = pTimer->TimerValue; if (dwCount > dwTick) { // This is an error case. Recover gracefully. dwCount = dwTick; } else { dwCount = dwTick - dwCount; } pCurTicks->QuadPart += dwCount; dwCount += *pPartialCurMSec; *pPartialCurMSec = dwCount % OEMCount1ms; *pCurMSec += (dwCount /= OEMCount1ms); return dwCount; } The following code example shows an implementation for the DDB5476 platform using a free-running timer with value and compare registers. DWORD CPUGetSysTimerCountElapsed( DWORD dwTimerCountdownMSec, volatile DWORD *pCurMSec, DWORD *pPartialCurMSec, Filename: HowToDevelopAnOAL.doc volatile ULARGE_INTEGER *pCurTicks ); { DWORD dwTick = dwTimerCountdownMSec * OEMCount1ms; DWORD dwCount = R4000Compare( ) - R4000Count( ); // Note: if dwCount is negative, the counter went past the compare // point. The math still works because it accounts // for the dwTick time plus the time past the compare point. dwCount = dwTick - dwCount; pCurTicks->QuadPart += dwCount; dwCount += *pPartialCurMSec; *pPartialCurMSec = dwCount % OEMCount1ms; dwCount /= OEMCount1ms; *pCurMSec += dwCount; return dwCount; } See Also Boilerplate Interface Routines 75 Filename: HowToDevelopAnOAL.doc 76 Adding KITL Initialization Code Kernel independent transport layer (KITL) support enables a debug communications channel with the development workstation through Platform Builder, and is essential for enabling kernel debugger support. If the KITL connection occurs over an Ethernet connection, the Ethernet debug (EDBG) library used in the boot loader development process can be used. The EDBG library will be referenced and linked into the kernel executable. For more information about KITL, see Kernel Independent Transport Layer. The following list shows the suggested tasks when adding KITL initialization code to the OEMInit function: • Initialize any PCI bridges and devices, and then enumerate, assign resources, and enable these. Note This should only be done for the KITL network interface card (NIC) and for any bridges required by the KITL NIC. • Perform any other bus initialization required for the CPU in order to recognize the NIC. • Initialize KITL by calling the KitlInit function. You can copy most of the code by using Halkitl.c from another platform. KitlInit must be called before any debug services can be started. For more information about KitlInit and enabling KITL, see Enabling KITL. • Implement the OEMKitlInit function, which searches for either KITL NIC, KITL serial, or parallel connections. For more information about implementing OEMKitlInit, see Implementing the OEMKitlInit Function. OEMKitlInit performs the search by calling the OEMEthInit function, which in turn performs the same tasks as the OEMPreDownload function in the boot loader. For more information about the tasks performed by OEMPreDownload, see Implementing the OEMPreDownload Function. • After implementing the OEMKitlInit function, include Kitl.lib and Kitleth.lib in your Platform\<Platform>\Kernel\Buildexe\Kernkitl sources file. After performing the above tasks, you can enable KITL polling. See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 77 Enabling KITL The KITL version of the OS image is placed in the release directory when the IMGNOKITL flag is not set, and the non-KITL image is released when IMGNOKITL is set to 1. KITL functionality can be included in both debug and retail devices, depending on your needs. It can also be configured to start on boot, called active KITL, or to start the first time a service on the device registers with KITL, called passive KITL. Passive KITL works with KITL in an inactive state until it is required. Passive KITL does not affect the overall performance of the device. For more information, see Active and Passive KITL. The KITLTRANSPORT structure defines a KITL transport, which provides all the KITL functionality. KITL needs to communicate with the underlining hardware including the ability to encode and decode a packet, send and receive a frame or packet, enable and disable the transport interrupt, and get and set configuration information. The following table shows the functions you need to implement to support this functionality. T Function Description TransportEncode Makes a transport ready packet that KITL can later send from the transport through TransportSend. The implementation frames the data with transport specific and checksum information. Using an Ethernet transport, TransportEncode frames the packet with a User Datagram Protocol (UDP) header. TransportDecode Called when KITL receives a raw packet from the transport through TransportRecv to extract the data field of the packet. Using an Ethernet transport, TransportDecode will validate the packet, checksum, and return the pointer immediately after the UDP header. TransportSend Called to send a raw packet, framed by TransportEncode, from the transport. KITL guarantees exclusive access to the transport when TransportSend is called. Using an Ethernet transport, TransportSend will program the hardware and place the packet on the Ethernet. TransportRecv Called to receive a raw packet from the transport. KITL guarantees exclusive access to the transport when TransportRecv is called. Using an Ethernet transport TransportRecv will read from the Ethernet hardware. TransportEnableInt Called to enable or disable transport interrupt. TransportGetDevCfg Called to provide device information to the debug host with the transport defining how the data appears. KITL handles the data as an opaque entity and sends it to the debug host. TransportGetDevCfg is only called at the initial handshake with the debug host. Using an Ethernet transport, the information usually includes IP address, MAC address, and port number. Filename: HowToDevelopAnOAL.doc TransportSetHostCfg 78 Called when KITL receives the transport information of the debug host with the transport defining how the data appears. KITL handles the data as an opaque entity and TransportSetHostCfg can use the information to setup the transport. TransportSetHostCfg is only called at the initial handshake with the debug host. KITL is initialized by calling KitlInit typically during the OEMInit phase of the boot process. KitlInit must be called before any debug services can be started. As part of the KitlInit processing, the kernel calls the OEMKitlInit function to perform hardware initialization. KitlInit may be called any time after the KernelStart function has been called, typically as part of OEMInitDebugSerial or OEMInit processing. KitlInit should also be called in the resume from suspend processing in OEMPowerOff. The parameter fStartKitl tells the kernel if it should start the IST at the time of call. If fStartKitl is FALSE, KITL behaves in a passive mode, waiting dormant until KITLRegisterClient is called. This technique is used on test devices that require the use of just-in-time (JIT) debugging. If fStartKitl is TRUE, KITL will start communicating with the desktop and will block until a connection is made. Also, when fStartKitl is set to TRUE the default services PPSH, DBGMSG, and KDBG are started based on the configuration settings from the desktop. Because KITL is used to attach all full-function debugging tools, if an error occurs during KITL initialization, use KITLOutputDebugString to output debug messages. KITLOutputDebugString eventually uses an OEM supplied function in the OAL for debug messages. The kernel then calls the OEM's routine. Once KITL is up and running, it is in a waiting state, waiting for data to be received by the transport and waiting for device side services to send data out. The typical call sequence is shown in the following code example. RecvData () { LockTransport (); Result = KITL. TransportRecv (pData, pcbShort); UnlockTransport (); If (Result) { pData = KITL.TransportDecode (pData, pcbShort); } } Filename: HowToDevelopAnOAL.doc 79 When the KITL protocol has data to send, it passes a buffer to the TransportEncode function that has space allocated for the header and trailer. TransportEncode adds the header and trailer to the frame and passes the encapsulated data back to the kernel. The kernel then calls TransportSend to send out the data to the desktop side, as shown in the following code sample. SendData (...) { if (KITL.TransportEncode (pData, cbShort)) { // pData has room for Hdr/Tlr allocated LockTransport (); KITL.TransportSend (pData, cbShort + KITL.FrmHdrSize + KITL.FrmTlrSize); UnlockTransport (); } } If your KITL transport requires a timer, the KITL protocol supports a 1 second resolution timer. The timer requires a call to KitlSetTimerCallback. The timer is a not a periodic timer and is only called once every nSecs seconds. If you need the timer to be called periodically, you can call KitlSetTimerCallback again when the timer expires. You can also request more than one timer. When the timer is no longer needed, call KitlStopTimerCallback to stop the timer. See Also How to Develop an OEM Adaptation Layer | Adding KITL Initialization Code Filename: HowToDevelopAnOAL.doc 80 Implementing the OEMKitlInit Function The following list shows the suggested tasks for the OEMKitlInit function: • Locate the KITL NIC, typically through driver globals. The KITL NIC is usually the same NIC used by the boot loader for download operations. • Assign function pointers to the correct EDBG NIC driver functions. If the necessary EDBG driver is not provided, you will need to create your own. The following code example shows how you can assign function pointers to an EDBG NIC driver. pfnEDbgInit = NE2000Init; pfnEDbgInitDMA = NULL; pfnEDbgEnableInts = NE2000EnableInts; pfnEDbgDisableInts = NE2000DisableInts; pfnEDbgGetPendingInts = NE2000GetPendingInts; pfnEDbgGetFrame = NE2000GetFrame; pfnEDbgSendFrame = NE2000SendFrame; pfnEDbgReadEEPROM = NE2000ReadEEPROM; pfnEDbgWriteEEPROM = NE2000WriteEEPROM; pfnEDbgSetOptions = NE2000SetOptions; pfnCurrentPacketFilter= Ne2000CurrentPacketFilter; pfnMulticastList = NE2000MulticastList; • Include the library in the Buildexe\Kernkitl sources file to satisfy the linker if any KITL driver support is required. • Initialize the DMA buffers for the NIC, if required, and then initialize the NIC by calling the pfnEdbgInitDMABuffer or pfnEdbgInit, or both, function pointers. The NIC initialization function takes a NIC address and returns a MAC value for the NIC, which should be stored in the driver globals. • Create a unique device name, which will be used as a handle by Platform Builder. Note The device name must be the same as the one used by the boot loader. • Obtain an IP address from a DHCP server using the EbootGetDHCPAddr function or through a static IP address. If you are obtaining the IP address from a DHCP server, include Eboot.lib in the Kernel\Buildexe\Kernkitl sources file. • Initialize the KITL Ethernet transport code by calling the KitlEtherInit function. If the call succeeds, fill out the KITLTRANSPORT structure. • Implement the following OEM Ethernet functions: • OEMEthCurrentPacketFilter • OEMEthDisableInts • OEMEthEnableInts • OEMEthGetFrame • OEMEthGetSecs • OEMEthMulticastList • OEMEthSendFrame Filename: HowToDevelopAnOAL.doc • 81 OEMEthSetFilter During the call to OEMKitlInit, you provide the following information that the kernel will need to support KITL high-level functionality: • Complete the KITLTRANSPORT structure that describes what services to start on boot • Determine whether it is a cold boot • Provide the name of the device • Provide an interrupt number for interrupt based devices • Provide the window size for default services • Provide the size of the frame header specific to your transport • Provide the size of the frame trailer specific to your transport • Provide the size of the physical buffer for default services • Provide function pointers to the transport-specific functions described above T When the kernel returns from OEMInit, it tries to initialize the KITL interrupt by calling the TransportEnableInt member of the KITLTRANSPORT structure. TransportEnableInt is also called when a device resumes from a suspended state. T See Also How to Develop an OEM Adaptation Layer | Adding KITL Initialization Code Filename: HowToDevelopAnOAL.doc 82 Implementing the Real-Time Clock and System Timer Windows CE provides a variety of functions that your OAL can use to manage time. These functions fall into two categories: real-time clock functions and system-timer functions. Implementing Real-Time Clock Functions Real-time clock functions manage time-of-day information. The file Rtc.c, located in %_WINCEROOT%\Platform\<Platform>\Kernel\Hal\<Microprocessor>, defines the interfaces that Windows CE uses to communicate with the kernel and the real-time clock. The following table shows the functions that you must implement on platforms that provide the time of day to the user. Function Description OEMGetRealTime Gets the system time. OEMSetRealTime Sets the system time. OEMSetAlarmTime Sets the alarm time. You can also use the IOCTL_HAL_INIT_RTC I/O control code in a call to KernelIoControl to force real-time clock initialization. For more information about how to set the real-time clock, see Setting the Real-Time Clock. Implementing the System-Timer Functions To query the system timer, Windows CE provides the GetTickCount function. Windows CE also provides a prototype implementation of the system-timer ISR. The OAL system-timer function, SC_GetTickCount, returns the current number of milliseconds since boot. Windows CE calls SC_GetTickCount in your OAL when the Microsoft Win32® function GetTickCount is called. You must register the system-timer interrupts in OEMInit to ensure the system timer is updated appropriately. Each sample platform provides a sample implementation of the system-timer ISR. Each time the interrupt goes off, the ISR should reset the hardware and increment the variable CurMSec by one millisecond. See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 83 Customizing Memory After calling OEMInit, the kernel calls the function pointed to by pNKEnumExtensionDRAM if provided, otherwise it calls OEMGetExtensionDRAM function to determine whether a contiguous bank of dynamic RAM (DRAM) is available on the target device. The kernel also calls this function when a user adds and removes memory. If a user adds memory, the kernel can continue processing without interruption. However, the kernel assumes that the additional memory, called extension memory, contains part of the object store, and therefore performs a cold boot to reinitialize the object store when a user removes the extension memory. While you can define memory areas in the Config.bib file, you can define extension memory using OEMGetExtensionDRAM, OEMEnumExtensionDRAM, and the MainMemoryEndAddress global variable only. Because the kernel will use any memory that it is aware of, you should not define any memory areas that you want to reserve for a driver. For more information about the Config.bib file, see Config.bib Customization. The following list shows the various ways you can customize how your system manages memory: • Configuring System Memory • Configuring Object Store Memory • Creating Physical to Virtual Mappings • Memory Addressing See Also How to Develop an OEM Adaptation Layer | Memory Architecture | Object Store and Registry Filename: HowToDevelopAnOAL.doc 84 Configuring System Memory The amount of RAM allocated to the kernel for an operating system (OS) use can be configured in four different areas: • Config.bib Customization • MainMemoryEndAddress Customization • OEMGetExtensionDRAM Customization • pNKEnumExtensionDRAM Customization In each case, the memory identified requires that the physical-to-virtual mapping tables are initialized so that the kernel can implement the proper support to use the memory. In the SHx and MIPS families, the CPU defines this mapping, and the kernel requires no additional information. In the x86 and ARM families, you must set up the proper entries in OEMAddressTable to identify your RAM regions. You should create RAM entries for the largest possible amount of RAM that would be present on the device. This help avoid the need to change the OEM adaptation layer (OAL) if you add more memory. If RAM is not mapped during the boot process through OEMAddressTable, it can be mapped later by calling CreateStaticMapping or NKCreateStaticMapping. However memory is specified to the kernel, the kernel will create a single pool that will be used for all memory allocations. See Also Customizing Memory | How to Configure and Build an OS Image for a CEPC Filename: HowToDevelopAnOAL.doc 85 Config.bib Customization The Config.bib file is the first place to declare how much memory will be made available to the kernel. A single entry is required and should indicate the default amount of RAM that is possible for the device. The following code example shows a sample Config.bib file. Section Name RAM Start RAM Address 80b00000 Number of Bytes 00500000 Section Type RAM Only one contiguous memory section of RAM can be reported in the Config.bib file. All other memory extensions must be handled through MainMemoryEndAddress, OEMGetExtensionDRAM, or pNKEnumExtensionDRAM customization. The start address of RAM and the number of bytes determine the value of ulRAMEnd (start + number of bytes) where ulRAMEnd is a parameter of the table of content specified in Romldr.h. ulRAMEnd is calculated and set in the OS image every time an OS image is created. See Also Configuring System Memory Filename: HowToDevelopAnOAL.doc 86 MainMemoryEndAddress Customization The MainMemoryEndAddress kernel variable is the only mechanism that allows you to extend the size of the contiguous RAM section specified in the Config.bib file. During the boot process, the kernel sets the value of MainMemoryEndAddress to the value stored in the table of contents ulRAMEnd. During OEMInit, you can detect the existence of additional contiguous RAM and reset the MainMemoryEndAddress value to indicate that there is more contiguous RAM available. The value of ulRAMEnd is based on the RAM section of the Config.bib file and what effect other flags have on ROMIMAGE. For example, the AUTOSIZE flag can use more or less memory than specified by the RAMIMAGE section and therefore affect the RAM size. See Also Configuring System Memory Filename: HowToDevelopAnOAL.doc 87 OEMGetExtensionDRAM Customization If there is a second region of RAM that is not contiguous with the RAM specified in the Config.bib file, OEMGetExtensionDRAM can be used to report the additional memory to the kernel. During the boot process, the kernel calls OEMGetExtensionDRAM to determine if there is a second region of RAM that the OEM wants to report to the kernel. If no second region is available, it returns FALSE. See Also Configuring System Memory Filename: HowToDevelopAnOAL.doc 88 pNKEnumExtensionDRAM Customization If there is a base RAM section plus more than one noncontiguous RAM section, you need to implement the pNKEnumExtensionDRAM function pointer. If you have a base RAM section and one or zero noncontiguous RAM sections, you do not need to implement this function pointer. The pNKEnumExtensionDRAM variable is capable of reporting up to 15 different noncontiguous RAM regions and like OEMGetExtensionDRAM, it is called during the cold boot phase. If pNKEnumExtensionDRAM is set, OEMGetExtensionDRAM is not called during the boot process. Note that when adding multiple sections that there is a performance impact for every memory section that is added to the kernel. The impact is due to the kernel tracking and searching for free physical memory in different memory sections. See Also Configuring System Memory Filename: HowToDevelopAnOAL.doc 89 Configuring Object Store Memory Once the available RAM is calculated, the kernel calculates how much memory will be allocated to the object store, Filesys.exe. The kernel first uses the FSRAMPERCENT value that is logged in Config.bib. During the boot process, it gives you access through pOEMCalcFSPages to change the default allocation. Once the final value is calculated, the kernel divides the memory between the kernel allocation used for running programs and the Filesys.exe allocation used for the object store. When the system is running, this division can be determined by calling GetSystemMemoryDivision or SetSystemMemoryDivision. See Also Customizing Memory Filename: HowToDevelopAnOAL.doc 90 Creating Physical to Virtual Mappings After the kernel has defined physical memory, it must create a physical-to-virtual mapping so that all physical memory is accessible to applications and interrupt service routines (ISRs). The physical-to-virtual mapping is a mapping from the physical address location of the RAM, or other types of memory, to a statically mapped virtual address that is used by applications and ISRs. In the 4-GB address space, the statically mapped virtual addresses live in the 3-GB range 0x80000000 to 0xc0000000. On MIPS and SHx platforms, this mapping is defined by the CPU but is not mapped through the memory management unit (MMU). On x86 and ARM processors, you must create an OEMAddressTable and pass it to the kernel. Each entry in the table specifies a physical location in memory, the size of the memory, and the static virtual memory address to which to map it. The static virtual address is specified in the cached memory range and the kernel can then create the uncached address that points to the same physical address. After the kernel creates the original mapping during the boot process, an application or the OAL can add to the statically mapped virtual address pool by calling CreateStaticMapping or NKCreateStaticMapping. Memory mapped in this way is located in range C400 0000 to E000 0000 and is created as uncached memory only. See Also Customizing Memory Filename: HowToDevelopAnOAL.doc 91 Memory Addressing There are two kinds of addresses in Microsoft® Windows® CE .NET: physical addresses and mapped virtual addresses. Physical addresses can be actual RAM or device memory that needs to be accessed by the OS. It is defined by the physical address space from a CPU perspective. The CPU cannot access this directly once the MMU is enabled. The kernel can only manage 512 MB of physical memory. On some CPUs, like MIPS and SHx, the kernel can only directly manipulate the first 1 GB of memory defined by the processors, 512 MB cached and 512 MB uncached but aliased to the same 512 MB of physical memory. On x86 and ARM processors, you can divide this physical range by using OEMAddressTable. Mapped virtual addresses define a mapping between the virtual and physical that can be used by user-mode and kernel applications. There are two types of mapped virtual addresses: static and dynamic. Static virtual addresses provide a mapping table for the kernel that covers what virtual address range maps to a physical address range. This mapping does not change over time and is created at boot time. OEMAddressTable defines this static virtual to physical mapping on the x86 and ARM processors. On MIPS and SHx, the CPU and kernel define this mapping. The static mapping can also be extended after boot time by calling CreateStaticMapping or NKCreateStaticMapping. Mappings created with CreateStaticMapping and NKCreateStaticMapping are accessible only by the kernel. A dynamically mapped virtual address is one that creates a virtual-to-physical mapping that can be defined and then released when not in use. This type of mapping is accessible to user-mode applications and is created by calling VirtualCopy. Using VirtualCopy with the PAGE_PHYSICAL flag, an application can map a physical address to a process-specific static virtual address. The process starts by calling VirtualAlloc to allocate a virtual address range in your process to map the physical address. This is followed by a call to VirtualCopy to complete the mapping process and make the physical memory available in the process. A driver typically uses VirtualCopy to map device-specific memory into its user-mode process space. The same region can be mapped by more than one process. CreateStaticMapping is like VirtualCopy in that it creates a mapping between physical memory and static virtual memory. The main difference is that the static virtual address is mapped in the kernel address space. This makes the static address accessible only by the kernel. CreateStaticMapping is typically used to map some device-specific memory into the kernel so that an ISR can access it. The same physical address can also be mapped by using VirtualCopy. See Also Customizing Memory Filename: HowToDevelopAnOAL.doc 92 Implementing the OEMIoControl Function Windows CE–based functions, such as SystemParametersInfo and KernelIoControl, supply information about the platform to applications. The Windows CE kernel obtains some of the platform-specific information by calling your implementation of the OAL function OEMIoControl. To better understand what IOCTLs to implement and to learn more about the implementation details for OEMIoControl, refer to a sample BSP that is based on hardware similar to your own. OEMIoControl can expose any number of IOCTLs. You can also define your own custom IOCTL, and some drivers expect specific IOCTLs to be implemented. For more information about the IOCTLs you can implement, see the Windows CE .NET Help. The following topics contain information about how you can implement OEMIoControl in your OAL to support various capabilities: • Supporting Interrupt Latency Timing • Providing System Information • Rebooting the Target Device • Setting the Real-Time Clock • Identifying Devices Uniquely See Also How to Develop an OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 93 Supporting Interrupt Latency Timing Interrupt latency timing measures the following two times: • The time between an interrupt being generated and its ISR being invoked. • The time between the ISR exiting and when the interrupt service thread (IST) begins scheduled. Latency timing is measured using Iltiming.exe and requires significant support from the OAL. The interrupt latency measurement leverages the fact that timer ISRs should occur at known times, on whole milliseconds. The number of timer ticks in dwIsrTime1 measures the interval between the timer interrupt occurring and the start of its ISR. Iltiming.exe creates an interrupt object for SYSINTR_TIMING. It obtains a kernel mode pointer to PerfCountSinceTick using one of the IOCTL_HAL_ILTIMING calls. When the timer ISR returns with a value of SYSINTR_TIMING, the interrupt service thread in Iltiming.exe is scheduled. The service thread runs in kernel mode, so it calls directly into the kernel's code space to get the current PerfCountSinceTick value. It subtracts dwIsrTime2 from this to measure the latency between when the ISR exits, presumably very close to the time the IST is scheduled, and when the IST actually starts. In some cases, other interrupts will occur between the time the Iltiming.exe IST is scheduled and when it starts executing. Iltiming.exe determines when this has happened by comparing dwIsrTime1 and dwIsrTime2; if an extra interrupt has occurred, dwIsrTime1 will be greater than dwIsrTime2. Iltiming.exe can also determine if extra interrupts have occurred by examining wNumInterrupts; this is why all ISRs need to set these variables. To support interrupt latency timing 1. Implement IOCTL_HAL_ILTIMING support in OEMIoControl. This code is hardware independent and can be copied from any of the sample platforms. It includes the global variables fIntrTime, dwIsrTime1, dwIsrTime2, and wNumInterrupts, among others that should all be copied. z. OEMInterruptEnable should return success for SYSINTR_TIMING without actually performing an action. aa. Implement PerfCountSinceTick and PerfCountFreq. PerfCountSinceTick is the number of counter ticks that have elapsed since the latest timer interrupt occurred. For more information, see PerfCountSinceTick and PerfCountFreq. bb. All ISRs must check fIntrTime, one of the global variables, at their entry point. If this variable is set, the ISR should set dwIsrTime1 to PerfCountSinceTick and increment wNumInterrupts. cc. If fIntrTime is set, the timer ISR should decrement dwIntrTimeCountdown and return SYSINTR_TIMING if it is zero. Otherwise, it should return SYSINTR_RESCHED or SYSINTR_NOP as normal. Once it is zero, dwIntrTimeCountdown should be reloaded from dwIntrTimeCountdownRef; this variable is initialized through an IOCTL_HAL_ILTIMING message. dd. When it returns SYSINTR_TIMING, the timer ISR needs to set dwIsrTime2 to PerfCountSinceTick. The sample implementation of OEMIdle already checks fIntrTime and handles interrupt latency timing as a special case. Filename: HowToDevelopAnOAL.doc See Also How to Develop an OEM Adaptation Layer | Implementing the OEMIoControl Function 94 Filename: HowToDevelopAnOAL.doc 95 PerfCountSinceTick and PerfCountFreq PerfCountSinceTick and PerfCountFreq access a high-performance counter that is synchronized with the thread timer. They typically use the thread timer itself. They are most effective when the underlying timer counts are fast enough to provide meaningful data. If the thread timer is running at a rate of one tick per millisecond, these routines will not provide much information. If the underlying counter runs at a rate of 100 ticks per millisecond or more, the latency timers will be much more useful. See Also Supporting Interrupt Latency Timing | High-Performance Counter Support Filename: HowToDevelopAnOAL.doc 96 ILTiming Support ILTiming.exe is a testing tool that allows an OEM to measure ISR and IST latency. The application that controls the test is in two parts and is released as source in %_WINCEROOT%\Public\Common\Oak\Utils\Iltiming and %_WINCEROOT%\Public\Common\Oak\Utils\It_load. ILTiming.exe works by using system resources to track the amount of time spent in trying to get to the ISR and time spent trying to get to the IST. ILTiming.exe interacts with the kernel and OAL through an IOCTL_HAL_ILTIMING. If the IOCTL_HAL_ILTIMING is supported and properly implemented, ILTiming.exe will be able to measure ISR and IST latency. IOCTL_HAL_ILTIMING is passed to the OEM through OEMIoControl. The parameter lpInBuf contains an ILTIMING_MESSAGE structure and the wMsg parameter contains a command that must be executed on behalf of the caller. The following table shows the values for the wMsg command. Value Description ILTIMING_MSG_ENABLE Enable the ILTiming process and set the frequency of the measurements. The frequency is how often, in ticks, before the next ISR and IST measurement are taken. ILTIMING_MSG_DISABLE Disable ILTiming functionality. ILTIMING_MSG_GET_TIMES Return the time information about an ISR measurement. ILTIMING_MSG_GET_PFN Return a function point to PerfCountSinceTick. The ILTiming.exe file requires that you implement the following two functions: • PerfCountSinceTick: The amount of time since the last system tick or secondary timer interrupt. • PerfCountFreq: The frequency of the system tick or secondary timer. Both functions must be implemented to support the high-resolution counter APIs QueryPerformanceCounter and QueryPerformanceFrequency. See Also Supporting Interrupt Latency Timing | Real-Time Measurement Tools Filename: HowToDevelopAnOAL.doc 97 Providing System Information You can implement OEMIoControl to support IOCTL_HAL_GET_DEVICE_INFO, which provides support for SystemParametersInfo, which queries or sets system-wide parameters and updates the user profile. When Windows CE calls OEMIoControl with IOCTL_HAL_GET_DEVICE_INFO in the dwIoControlCode parameter, the OS sets the nInBufSize parameter to 4 bytes and passes a system parameters information (SPI) code in the lpInputBuf parameter. In order to support the SPI code, OEMIoControl must support the following SPI flags: Flag Description SPI_GETPLATFORMTYPE Requests a string that identifies the Windows CE–based platform type. Do not localize this string. SPI_GETOEMINFO Requests OEM-specific information. The information string can include, but is not limited to, the model name, model number, and manufacturer. Although Windows CE does not restrict the lengths of the returned strings, long strings may be unsuitable for display on some target devices or for communicating between applications. Also, be sure to use SPI_GETPLATFORMTYPE names that are unique across Windows CE products. Some host-side applications rely on this name for configuration. Using unique names prevents target device type conflicts and ensures successful integration with other Microsoft products. The following code example shows how to handle SPI_GETPLATFORMTYPE and SPI_GETOEMINFO requests. // OemIoControl implementation in %_WINCEROOT%\Platform\%BSP%\Kernel\Hal\Oemioctl.c: const WCHAR HALPlatformStr[] = L"My platform"; const WCHAR HALOEMStr[] = L"Test"; BOOL OEMIoControl(...) { ... switch (dwIoControlCode) { case IOCTL_HAL_GET_DEVICE_INFO: if (nInBufSize == 4) { // contains the SPI_* code switch (*(LPDWORD)lpInBuf) { case SPI_GETPLATFORMTYPE: len = (strlenW(HALPlatformStr)+1)*sizeof(WCHAR); if (nOutBufSize >= len) { memcpy(lpOutBuf,HALPlatformStr,len); retval = TRUE; Filename: HowToDevelopAnOAL.doc } else SetLastError(ERROR_INSUFFICIENT_BUFFER); break; case SPI_GETOEMINFO: len = (strlenW(HALOEMStr)+1)*sizeof(WCHAR); if (nOutBufSize >= len) { memcpy(lpOutBuf,HALOEMStr,len); retval = TRUE; } else SetLastError(ERROR_INSUFFICIENT_BUFFER); break; default: SetLastError(ERROR_INVALID_PARAMETER); } } else { SetLastError(ERROR_INVALID_PARAMETER); } break; // End switch case for IOCTL_HAL_GET_DEVICE_INFO ... See Also How to Develop an OEM Adaptation Layer | Implementing the OEMIoControl Function 98 Filename: HowToDevelopAnOAL.doc 99 Rebooting the Target Device You have the option of implementing IOCTL_HAL_REBOOT in OEMIoControl to reboot the target device. For example, an application can display a dialog box to query the user to reboot the target device, and then call IOCTL_HAL_REBOOT to perform the reboot. Platform Builder provides a sample implementation in %_WINCEROOT\Platform\Cepc\Kernel\Hal. See Also How to Develop an OEM Adaptation Layer | Implementing the OEMIoControl Function Filename: HowToDevelopAnOAL.doc 100 Setting the Real-Time Clock Some target devices cannot detect whether the real-time clock is initialized, and therefore cannot determine when to reset it. For example, a computer-based target device that has a clock with a battery backup may be unable to detect real-time clock initialization. You change this behavior by implementing the I/O control code IOCTL_HAL_INIT_RTC in OEMIoControl. The Windows CE–based file system passes IOCTL_HAL_INIT_RTC to KernelIoControl during initialization for a cold boot sequence. The following code example shows the SYSTEMTIME structure that the file system passes. const SYSTEMTIME st = {1999,6,0,1,12,0,0,0}; KernelIoControl(IOCTL_HAL_INIT_RTC, (UCHAR *)&st, NULL, 0, &bytesUsed); To set the real-time clock in OAL code 1. Validate the clock during the OEM boot process in your OAL code. ee. Set the real-time clock to the value stored in SYSTEMTIME, if the boot process did not return a valid time. ff. Call OEMSetRealTime in OEMIoControl to set the real-time clock of the target device. The following code example shows one implementation of setting the real-time clock. case IOCTL_HAL_INIT_RTC: // The kernel has detected a cold boot. // The real-time clock probably needs to be reset. if( nInBufSize >= sizeof(SYSTEMTIME) ) return OEMSetRealTime( (LPSYSTEMTIME)lpInBuf ); else return FALSE; break; See Also How to Develop an OEM Adaptation Layer | Implementing the OEMIoControl Function Filename: HowToDevelopAnOAL.doc 101 Identifying Devices Uniquely You may need to implement a mechanism to uniquely identify a Windows CE–based device. For example, you may need to identify each cell phone that runs your OS image for billing and security purposes. Windows CE uses the DEVICE_ID structure to hold the unique device identification number. You can implement the I/O control code IOCTL_HAL_GET_DEVICEID in OEMIoControl to return DEVICE_ID to an application. See Also How to Develop an OEM Adaptation Layer | Implementing the OEMIoControl Function Filename: HowToDevelopAnOAL.doc 102 Completing an OAL Completing an OAL is the final step of creating an OAL. In this stage, you can implement any other features you choose to support. To complete an OAL 1. Create a CEDDK. For more information, see Implementing CEDDK.dll. gg. Erase the object store. For more information, see Erasing the Object Store. hh. Implement support for high-resolution timers. For more information, see Supporting High-Resolution. ii. Configure the process boot phase. For more information, see Configuring the Process Boot Phase. See Also How to Develop an OEM Adaptation Layer | OEM Adaptation Layer Filename: HowToDevelopAnOAL.doc 103 Erasing the Object Store The object store for Windows CE is persistent across warm boots as long as the boot process does not cause other code that changes the contents of RAM, such as hardware initialization code, to get executed. On occasion, you may want to completely erase the object store by triggering a cold reboot. For example, you may create a prototype that tests the cold system boot process. While an application can call the SetCleanRebootFlag function to perform a cold boot, you can also implement the following procedure in the boot process. To erase the object store during the boot process 1. Include the Romldr.h file in your OAL code. jj. Declare the following variable in your OAL. extern ROMHDR *const volatile pTOC; The ulRAMFree member of the ROMHDR structure identifies the start of free memory. The UlRAMFree location also identifies the start of the object store information. kk. Signal a cold boot in OEMInit by setting the first three DWORD variables starting at pTOC->ulRAMFree, to 0. The following code example shows how to set the first three DWORD variables to 0. memset(pTOC->ulRAMFree,0,sizeof(DWORD)*3) Setting the first three DWORD values to 0 during OEMInit ensures that a cold boot of the object store occurs during the boot process. See Also Completing an OAL Filename: HowToDevelopAnOAL.doc 104 Supporting High-Resolution Timers The high-resolution timer functions QueryPerformanceCounter and QueryPerformanceFrequency allow independent software vendors (ISVs) to use the high-resolution timers available on your target device. These high-resolution timers enable more accurate timings than the one-millisecond granularity available through the GetTickCount function. For more information, see High-Performance Counter Support. The OAL sample code provides default implementations of the performance querying functions. If you do not provide high-resolution timers on your platform, the default capabilities of the performance querying functions are implemented using GetTickCount. In the default code, a call to QueryPerformanceCounter returns the value represented by GetTickCount, while QueryPerformanceFrequency returns 1000. To support high-resolution timers 1. Provide your own implementations of the OEMQueryPerformanceCounter and OEMQueryPerformanceFrequency functions. ll. Add code to OEMInit to set the variables pQueryPerformanceCounter and pQueryPerformanceFrequency to point to your implementations of the OEMQueryPerformanceCounter and OEMQueryPerformanceFrequency functions. mm. Make any further calls to the QueryPerformance functions through their respective pointers. See Also Completing an OAL Filename: HowToDevelopAnOAL.doc 105 High-Performance Counter Support There are two built-in high-performance counter routines, QueryPerformanceCounter and QueryPerformanceFrequency. QueryPerformanceCounter returns a 64-bit number representing timer counts and QueryPerformanceFrequency returns the number of high-performance ticks per second. It is used to convert counts to a time value. In many cases, platforms do not implement 64-bit high-resolution counters in hardware. A workaround is to implement the counter using 32-bit hardware counters and implement the rollover to 64-bits through an interrupt. Another is to use the thread scheduling timer to update a 64-bit tick count. Like PerfCountSinceTick, this works well if the underlying hardware timer has a large number of ticks per millisecond. Otherwise, OEMQueryPerformanceCounter does not offer much of an advantage over GetTickCount. If the thread timer tick will meet your needs, you can use the boilerplate implementations of QueryPerformanceCounter and QueryPerformanceFrequency provided with the sample platforms. The sample routines are implemented in terms of PerfCountSinceTick and PerfCountFreq, which are also used in interrupt latency timing. See Also Supporting High-Resolution Timers | Implementing the OEMInit Function | PerfCountSinceTick and PerfCountFreq Filename: HowToDevelopAnOAL.doc 106 OSBench Support OSBench.exe is used to measure the performance of the scheduler. The measured performance is stored in two areas: %_WINCEROOT%\Public\Common\Oak\Utils\Osbench and %_WINCEROOT%\Public\Common\Oak\Utils\Ob_load. OSBench.exe works by using system resources to track the amount of time required to perform some scheduling tasks. The only requirement that OSBench.exe has on the OEM is to support the QueryPerformanceCounter and QueryPerformanceFrequency. See Also Supporting High-Resolution Timers | Real-Time Measurement Tools Filename: HowToDevelopAnOAL.doc 107 Configuring the Process Boot Phase When you finish adapting your OAL, you can set the system registry for your platform. When Windows CE begins loading, the kernel starts the file system and examines the HKEY_LOCAL_MACHINE\Init registry key to identify what applications to run. Although you determine which applications the OS loads, most platforms include the following applications: • Debug shell (Shell.exe) Shell.exe communicates with Cesh.exe application running on your development workstation. Include only Shell.exe in your OS image for debugging and development. Shell.exe displays the Windows CE prompt in the command prompt build window after you use Cesh.exe to transfer the Windows CE image. • Device Manager (Device.exe) • Graphics, Windowing, and Events Subsystem (Gwes.exe) Gwes.exe also loads and initializes input device drivers such as the keyboard driver and the touch driver. • Task Manager (Taskman.exe) To control which applications run at system startup, create launch registry values. Launch registry values do not need to be sorted in the registry, although you can specify dependencies. You can specify up to 32 applications. The following table shows the entries for the HKEY_LOCAL_MACHINE\Init registry key. Entry Value Type Launchnn program.exe REG_SZ Dependnn hex:xx,yy[,xx,yy] REG_BINARY (yy is the most significant bit) Launch registry values have optional dependencies as denoted by the Dependnn registry value. Dependnn registry values specify applications that Windows CE must be running before the Launchnn applications run. Dependnn registry values begin with the keyword Depend, followed by the same decimal number as the Launchnn registry value. The Dependnn registry values define an order in which Windows CE launches applications. One or more dependent applications can be specified per Dependnn value. Dependent applications are specified as a series of Words in hexadecimal notation. The following code example shows a typical Init registry entry using dependencies. [HKEY_LOCAL_MACHINE\Init] "Launch10"="shell.exe" "Launch20"="device.exe" "Launch30"="gwes.exe" "Depend30"=hex:14,00 "Launch50"="taskman.exe" "Depend50"=hex:14,00, 1e,00 In the preceding example, Gwes.exe is dependent on Device.exe starting and Taskman.exe is dependent on Device.exe and Gwes.exe starting. Filename: HowToDevelopAnOAL.doc 108 After Windows CE calls an application using the Init registry value, the application must call the SignalStarted function. SignalStarted indicates that the application is ready for the rest of the OS to continue processing. The value that is passed as the parameter to SignalStarted is the value passed on the command line of the application started from the Init key. For more information about the format of the registry, see Registry File. For information about default registry settings, see Registry Settings. See Also Completing an OAL Filename: HowToDevelopAnOAL.doc 109 Testing an Enhanced OAL Windows CE loads Filesys.exe before any other applications. To test your OAL, create a test application that replaces Filesys.exe and prints in a loop by incrementing seconds using Sleep and GetTickCount. To test the OAL 1. Call the GetTickCount function. nn. Print the value returned by GetTickCount. oo. Call the Sleep function to suspend the thread. Monitor the duration of the sleep period. pp. Call GetTickCount again. qq. Print the value returned by GetTickCount. rr. Compare the two values returned by GetTickCount. The second call to GetTickCount should return the amount of time that has elapsed since the previous call to this function. Therefore, the value returned by the second call to GetTickCount should be nonzero and should not match the value returned from the first call to this function. Note GetTickCount returns time in the form of milliseconds. If the value returned by the second call to GetTickCount matches the amount of time that your system was in a suspended state, then you have successfully created a base OAL. See Also How to Develop an OEM Adaptation Layer
© Copyright 2024