This article is part of theEmbedded Softwareseries:Ada for the Embedded C Developer
We've seen in the previous articles how Ada can be used to describe high-level semantics and architecture. The beauty of the language, however, is that it can be used all the way down to the lowest levels of the development, including embedded assembly code or bit-level data management.
表示条款
One very interesting feature of the language is that, unlike C, for example, there are no data representation constraints unless specified by the developer. This means that the compiler is free to choose the best tradeoff in terms of representation vs. performance. Let's start with the following Ada example:
type R is record V : Integer range 0 .. 255; B1 : Boolean; B2 : Boolean; end record with Pack;
and C example:
struct R { unsigned int V:8; bool B1; bool B2; };
The Ada and C++ code above both represent efforts to create an object that's as small as possible. Controlling data size isn’t possible in Java, but the language does specify the size of values for the primitive types.
Although the C++ and Ada code are equivalent in this example, there's an interesting semantic difference. In C++, the number of bits required by each field needs to be specified. Here, we're stating that V is only 8 bits, effectively representing values from 0 to 255.
In Ada, it's the other way around: The developer specifies the range of values required and the compiler decides how to represent things, optimizing for speed or size. The Pack aspect declared at the end of the record specifies that the compiler should optimize for size even at the expense of decreased speed in accessing record components. We'll see more details about the Pack aspect in the sections aboutbitwise operationsandmapping structures to bit-fieldsin chapter 6 (to come).
也可以指定其他表示条款,以及根据可用值和指定尺寸的要求之间的编译时间一致性检查。当需要特定布局时,这特别有用。例如,与硬件,驱动程序或通信协议接口时。这是根据上一个示例指定特定数据布局的方法:
type R is record V : Integer range 0 .. 255; B1 : Boolean; B2 : Boolean; end record; for R use record -- Occupy the first bit of the first byte. B1 at 0 range 0 .. 0; -- Occupy the last 7 bits of the first byte. -- as well as the first bit of the second byte. V at 0 range 1 .. 8; -- Occupy the second bit of the second byte. B2 at 1 range 1 .. 1; end record;
We omit the “with Pack” directive and instead use a record representation clause following the record declaration. The compiler is directed to spread objects of type R across two bytes. The layout we're specifying here is fairly inefficient to work with on any machine. However, you can have the compiler construct the most efficient methods for access, rather than coding your own machine-dependent bit-level methods manually.
Embedded Assembly Code
When performing low-level development, such as at the kernel or hardware driver level, there can be times when it’s necessary to implement functionality with assembly code.
Every Ada compiler has its own conventions for embedding assembly code, based on the hardware platform and the supported assembler(s). Our examples here will work with GNAT and GCC on the x86 architecture.
所有X86处理器自英特尔奔腾提供RDTSC指令以来,它告诉我们自上次处理器重置以来的周期数。它不需要输入,并且在EDX和EAX寄存器之间分配了无符号的64位值。
GNAT provides a subprogram called System.Machine_Code.Asm that can be used for assembly code insertion. You can specify a string to pass to the assembler as well as source-level variables that can be used for input and output:
-- get_processor_cycles.adb with Ssytem.Machine_Code; use System.Machine_Code; with Interfaces; use Interfaces; function Get_Processor_Cycles return Unsigned_64 is Low, High : Unsigned_32; Counter : Unsigned_64; begin Asm (“rdtsc”, Outputs => (Unsigned_32’Asm_Output (“=a”, High), Unsigned_32’Asm_Output (“=d”, Low)), Volatile => True); Counter := Unsigned_64 (High) * 2 ** 32 + Unsigned_64 (Low); return Counter; end Get_Processor_Cycles;
上面的unsigned_32'asm_output子句提供了要更新的机器寄存器和源级变量之间的关联。= a和= d分别指EAX和EDX机器寄存器。来自软件包接口的Unsigned_32和Unsigned_64类型的使用可确保数据的正确表示。我们组装两个32位值以形成单个64位值。
We set the Volatile parameter to True to tell the compiler that invoking this instruction multiple times with the same inputs can result in different outputs. This eliminates the possibility that the compiler will optimize multiple invocations into a single call.
With optimization turned on, the GNAT compiler is smart enough to use the eax and edx registers to implement the High and Low variables. This results in zero overhead for the assembly interface.
The machine code insertion interface provides many features beyond what was shown here. More information can be found in the GNAT User's Guide, and the GNAT Reference manual.
Interrupt Handling
Handling interrupts is an important aspect when programming embedded devices. Interrupts are used, for example, to indicate that a hardware or software event has happened. Therefore, by handling interrupts, an application can react to external events.
ADA为处理中断提供内置支持。我们可以通过将处理程序(必须是受保护的程序)附加处理来处理中断。在声明受保护程序的声明中,我们使用附加_Handler方面,并指示我们要处理的中断。
Let's look into a code example thattrapsthe quit interrupt (SIGQUIT) on Linux:
-signal_handlers.ads with system.os_interface;Package signal_handlers是受保护的类型quit_handler请求的函数返回布尔值;私人quit_request:boolean:= false;- - 声明“退出”中断的中断处理程序 - end quit_handler;end Signal_hander;-signal_handlers.adb带有ada.text_io;使用ada.text_io;软件包Body Signal_handler受保护的身体quit_handler是函数请求返回布尔值(quit_request);procedure handle_quit_signal是开始put_line(“检测到退出请求!”);quit_request:= true; end Quit_Handler; end Signal_Handler; -- test_quite_handler.adb with Ada.Text_IO; use Ada.Text_IO; with Signal_Handlers; procedure Test_Quit_Handler is Quit : Signal_Handlers.Quit_Handler; begin while True loop delay 1.0; exit when Quit.Requested; end loop; Put_Line (“Exiting application...”); end Test_Quit_Handler;
的规范Signal_Handlers包from this example contains the declaration of Quit_Handler, which is a protected type. In the private part of this protected type, we declare the Handle_Quit_Signal procedure. By using the Attach_Handler aspect in the declaration of Handle_Quit_Signal and indicating the quit interrupt (System.OS_Interface.SIGQUIT), we're instructing the operating system to call this procedure for any quit request. So, when the user presses CTRL+\ on their keyboard, for example, the application will behave as follows:
- The operating system calls the Handle_Quit_Signal procedure, which displays a message to the user ("Quit request detected!") and sets a Boolean variable—Quit_Request, which is declared in the Quit_Handler type:
- 主要应用程序通过将请求的函数拨打为true循环的一部分来检查退出处理程序的状态:
*This call is in the exit when Quit.Requested line.
*The Requested function returns True in this case because the Quit_Request flag was set by the Handle_Quit_Signal procedure.
- The main applications exits the loop, displays a message, and finishes.
Note that the code example above isn't portable because it makes use of interrupts from the Linux operating system. When programming embedded devices, we would use instead the interrupts available on those specific devices.
另请注意,在上面的示例中,我们在编译时声明了一个静态处理程序。如果您需要使用可以在运行时配置的动态处理程序,则可以利用ADA.Intruck软件包中的子程序。该软件包不仅包含actact_handler的版本作为一个过程,还包括其他过程,例如:
- Exchange_Handler, which lets us exchange, at runtime, the current handler associated with a specific interrupt by a different handler.
- Detach_Handler, which we can use to remove the handler currently associated with a given interrupt.
Details about the Ada.Interrupts package are out of scope for this course. We'll discuss them in a separate, more advanced course in the future. You can find some information about it in theInterrupts appendix of the Ada Reference Manual.
Read more from theEmbedded Softwareseries:Ada for the Embedded C Developer