嵌入式软件时序(1)— C语言是怎么编译出来的

当很多汽车软件的开发人员都在忙于掌握AUTOSAR、自动驾驶、V2X、功能安全和信息安全等热门的技术时,我们往往忽略了汽车这个控制对象所要求的最重要的一个特性——“实时性”。今天,实时性这个专题已经上升到了“时序”这一概念和话题。传统的“实时性”仅仅只是针对某个功能和任务而言的,而“时序”不仅包含单一任务实施性的范畴,更加强调的是整个系统的各个任务如何设置、协作来达到全系统的所有任务的实时性要求。有幸发现了一本这方面最新的技术宝典,能够介绍完整的时序技术理论。概述的作者Peter Gliwa是专门从事汽车软件时序技术工作的专家,并以自己的名字命令了他的公司,在业界非常有名的T1时序工具就是出自Gliwa。在这个专题中我将把该书的精华逐一介绍分享,希望时序技术能够得到越来越多的关注。

软件开发和操作系统领域的基础知识是分析和优化嵌入式软件时序(Timing)的前提。不仅是针对那些想学习或复习基础原理的人,有经验的软件开发人员也会发现很有帮助。

软件开发的V模型

V模型描述了软件开发过程的概念。它在汽车行业已经使用了几十年,即使当前比较流行的敏捷开发模式(如Scrum),在背后仍然有V模型的身影。像许多技术的发展一样,它起源于军事部门,后来转入民用领域。

V模型中的 "V"代表了理想化的开发过程,有左右两各维度。横坐标是以项目开始为起点的时间轴。纵坐标标志着抽象:从底部的"详细"到顶部的 "抽象"。见下图,一个项目应该从收集客户对产品的需求开始,具有较高的抽象性。之后在系统层面上是产品的基本设计。在项目进一步的过程中,对设计进行分解、细化。如果可能,要进一步对产品产生更详细的需求。设计阶段完成后,开始进入实现阶段。就软件项目而言,这对应的是编码、各个组件的集成,然后是各个抽象层次上的验证。在这个过程中,要对之前制定的各层级的需求进行检查。最后的检查是在顶层抽象层进行,确保满足客户的需求。

如果不符合需求,就必须消除造成偏差的原因。原因必然是在V上需求和验证之间的某个点。因此,所有依附的后续步骤也必须进行修正、调整或至少是重复。

显然,错误发现得越晚,付出的代价越大。这个道理尽人皆知,但令人惊讶的是,很多项目对待嵌入式软件的时序特性并不关注。很多时候,运行时(Run-time)问题都是在项目后期才调查并临时修复的,匆忙之下,成本高,风险大。

时序特性与V模型

几乎每个汽车领域的软件开发人员都熟悉V模型。在使用V模型时,通常会把重点放在功能方面。当时序的话题出现时,会有什么不同?原则上,没有什么变化。该模型的基本思想也可以应用于时序特性。下图显示了这一思想的具体化,并给出了V模型不同阶段的时序特性相关技术点的例子。

C语言的编译:从模型到可执行文件

V模型的左半边和将源代码变成可执行文件的过程,即构建过程(build process)之间存在相似性。它从一个比较高的抽象层次开始,在时间的推移中,它越来越接近执行的硬件,处理器。

 

以下介绍了如何将源代码变成可执行的机器代码,以及哪些文件、工具和编译步骤是相关的。涉及的基础知识只是间接涉及到时序的话题。然而,如果不了解编译器的基本工作原理等,以减少运行时为目标的代码优化只能是困难重重。

· 基于模型的软件开发及代码生成

到现在,可以说在汽车上运行的软件中,基于模型的软件所占比例较大。这意味着源代码不是由人工编写的,而是由编码生成工具如Embedded Coder、Targetlink或ASCET生成的。之前,功能(通常是控制技术、数字滤波或状态机)是用MATLAB/Simulink或ASCET等图形化建模工具定义的,并以"模型"的形式保存。

· C语言预处理(Preprocessor)

以下代码显示了一个简单的程序,在本例中是手工编码的。下面将用这个程序来说明从源代码到可执行程序的过程。

包含的头文件myTypes.h的代码如下

代码第12行使用的关键字volatile(英文 "volatile "的意思是 "易失性")使编译器每次对受影响的变量的访问都是在内存中显性地进行的,而不是例如将其暂时存储在寄存器中。例如,当受影响的内存地址被硬件外设写入时,变量能够马上反映出内存内容的变化。硬件定时器的寄存器就是一个例子。volatile用于防止编译器将代码优化掉,即编译器认为变量a从未被"有意义地"使用,因此删除对它的所有访问的语句。

下图显示了从源代码到可执行文件的过程中要经过哪些步骤,涉及哪些中间格式,涉及哪些附加文件。可选的数据流、文件和工具以淡色显示。

 

第一步,编译器对代码进行预处理,解析所有的宏(" #define "),读取所有包含的头文件(" #include "),删除条件编译不满足的代码(" #ifdef (...) #endif "),并计算所有此时已经可以计算的值(" 400 * 5 / 1000 " → " 2 ")。所有以 "#"开头的语句都是预处理语句。事实上,预处理器还做了不少任务,但目前给出的例子应该足以说明其原理。

 

提示:大多数编译器都支持-E命令行选项,它使编译器在预处理阶段后终止,并将预处理代码输出到stdout。在调试与预处理有关的问题时,这一点非常有用。

 

此外,这个输出也很适合向编译器制造商报告编译器问题。如果输出被重定向到一个文件(文件扩展名为.i),这个文件可以传递给编译器进行编译,而不需要任何其他文件,比如包含的头文件。编译器厂商就可以重现问题,而不需要访问所有包含的头文件。

main.c的预处理输出(mian.i)

· C语言编译器(Complier)

预处理器的输出进入编译器,编译器从中生成处理器专用的机器代码,即与C代码对应的机器指令文件。此时函数、变量、跳转地址等内存地址还没有指定,而是以符号方式记录下来。

 

编译器的输出(在本例中是英飞凌AURIX处理器的TASKING编译器)可以部分从以下代码中看到。由于这段代码作为后续阶段汇编器的输入,所以也称为汇编代码。

 

编译器输出汇编代码 main.src

· 编译器代码优化

在将源代码翻译成机器代码时,编译器可以进行各种优化。这些优化中的很多都降低了对内存的要求,同时也带来了更快的代码执行速度。然而,在一些优化的情况下,一个方面的改进只会以牺牲另一个方面为代价。软件的开发人员必须决定哪个更重要。优化后的实际效益往往难以预先估计。即使是专家,也往往会对优化的结果大吃一惊。

· 汇编器(Assembler)

汇编器将汇编代码的汇编机器指令翻译成二进制形式。因此,汇编器的输出已经不容易被人类读懂,这里不再介绍。汇编文件(通常文件扩展名为.src或.s)称为“对象文件”。和之前的汇编代码一样,函数、变量、跳转地址等的内存地址在对象代码中还没有定义,但仍然可以用专门符号来表示。

· 链接器(Linker)

链接器将传递给它的所有对象组装成一个几乎已经完成的程序,只有具体的地址还没有分配。在我们的例子中,只传递了一个对象,即main.o。另外还隐含了一些对象,例如cstart.o,用于执行main函数前所需的基本初始化。这包括初始化内存,初始化堆栈指针,以及初始化变量。

 

此外,函数库可以传递给链接器,通常文件扩展名为.a或.lib。函数库实际上不过是对象的集合。 “归档器”(Archiver)将选定的对象打包成档案,非常类似于压缩程序("ZIP")或tarball生成器。

 

链接器的另一个任务是解析所有引用的符号。假设例子中的main函数会调用另一个函数SomeOtherFunction,这个函数之前会通过外部声明的方式被声明。这个声明可以是这样的:int SomeOtherFunction(int someParam);。

 

如果这个函数在main.c中没有实现,那么链接器就会将符号SomeOtherFunction记忆为一个已经被引用但尚未定义的函数,即解析。在所有进一步传递给链接器的对象中,链接器现在搜索符号SomeOtherFunction 。如果它找到了一个定义,即函数的实现,对符号的引用就会被解析。在所有对象都被搜索引用解析后,调用链接器时传递的函数库被用来解析剩余的引用。

 

如果在这里搜索不到某个符号,链接器就会报告一个错误,通常是 "unresolved external <symbol name>"。

 

如果一个符号被定义在一个以上的对象中,链接器也会报告一个错误,在这种情况下是"redefinition of symbol < symbol name >"。

 

如果一个符号被定义在一个对象和一个或多个函数库中,那么链接器会忽略函数库中的符号定义,并且不会报警或报错。

 

传递给链接器的函数库的顺序决定了搜索顺序。如果一个符号被解析了,所有后续的定义都会被忽略,不会被 "链接"。

· 定址器(Locator)

绝大多工具厂家都是将链接器和定位器合二为一,然后称之为链接器(Linker)。定址器的作用源于它的名字:它能 "定位"可用内存中的所有符号。每个单独符号的内存地址就是这样确定的。

 

定址器的输出最终是以带或不带符号信息的格式输出的可执行文件。符号信息对于方便软件的调试是必需的。例如,当显示变量的内容时,符号信息可以方便地对应所需变量的名称,不需要手工对内存地址与变量名进行繁琐的匹配处理。

 

没有符号信息的可执行文件的典型输出格式是Intel HEX文件(*.hex)或Motorola S-Records(*.s19)。最常见的带有符号信息的可执行文件的输出格式是ELF格式(*.elf )。ELF是 "Executable and Linking Format "的缩写。

 

除了可执行文件外,还可以创建一个链接器map文件。其中包含了所有符号的列表以及它们的内存地址。

· 链接脚本

链接器脚本(也叫链接器控制文件)起着非常重要的作用。严格来说,应该叫 "Locator脚本 "或 "Locator控制文件",但如前所述,大多数厂家将定址器和连接器合并为"链接器"。

MCU的GNU ld链接脚本示例

以上显示了一个简单的8位MCU Microchip AVR ATmega32的链接器脚本片段,该MCU具有32 KByte Flash、2 KByte RAM和1 KByte EEPROM。

 

链接器脚本告诉定位器如何将符号分配到MCU的不同内存地址区域中。首先,在C语言或汇编源代码中,所有的符号都被分配到特定的“段”(section),更准确地说是“输入段”(input section)。即使程序员没有显式地进行这种操作,也会隐式地进行。以下是常见的段的名称,代表了默认段。段以“.”开头已成为惯例。接下来,链接脚本中的指令将所有的输入段分配给输出段(output section),而输出段又最终被映射到可用的内存地址中。在典型的链接脚本中,可用内存地址区域的定义在开头就能找到。然后按照输出段的定义,输入段与分配的内存区域关联到一起。

.text      程序代码

             例如: int GiveMe42(void){return 42;}

 

.rodata   只读数据(read-only)

              例如: const int a = 5;

 

.bss        可读写数据,初始化为0

               例如: int a;

               根据C语言标准,未初始化的全局变量必须被启动代码初始化为0。并非所有的嵌入式软件项目都是这样实现启动代码的,因此,不应理所当然地认为定义期间未赋初值的所有变量在启动时都会被清0。

 

.data      可读写数据 (read-write),初始化为一个指定值

              例如: int a = 5;

 

.noinit    可读写数据 (read-write),不初始化

              例如: int a;

              这个例子与.bss的例子相同。编译器开关通常可以用来控制代码中未初始化的变量是否应该用0初始化或根本不初始化。

 

.debug   调试段既不包含程序的代码,也不包含程序的数据,而是包含能够实现或简化软件调试的附加信息。

在GNU Linker手册中可以找到关于这个链接脚本的语法和基本概念的非常好的描述。大多数其他工具厂商的链接器至少采用了GNU链接器("ld")的概念。

 

链接脚本与时序有什么关系?访问的位置和类型对内存的访存时间和访问代码的执行时间有很大影响。访问的位置和类型是由链接脚本决定的,因此,对其语法和功能的了解对运行时的优化至关重要。

*本文所有示意图和代码示例,以及部分内容均引用自Peter Gliwa的《Embedded Software Timing》一书。

<本文首发于零束开发者论坛>

​作者:SoftAuto

文章来源:上汽零束SOA开发者论坛

原文链接:https://bbs.z-onesoft.com/omp/community/front/api/page/mainTz?articleId=7567

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
This tutorial reference takes the reader from use cases to complete architectures for real-time embedded systems using SysML, UML, and MARTE and shows how to apply the COMET/RTE design method to real-world problems. The author covers key topics such as architectural patterns for distributed and hierarchical real-time control and other real-time software architectures, performance analysis of real-time designs using real-time scheduling, and timing analysis on single and multiple processor systems. Complete case studies illustrating design issues include a light rail control system, a microwave oven control system, and an automated highway toll system. Organized as an introduction followed by several self-contained chapters, the book is perfect for experienced software engineers wanting a quick reference at each stage of the analysis, design, and development of large-scale real-time embedded systems, as well as for advanced undergraduate or graduate courses in software engineering, computer engineering, and software design. Table of Contents Part I: Overview Chapter 1. Introduction Chapter 2. Overview of UML, SysML, and MARTE Chapter 3. Real-Time Software Design and Architecture Concepts Part II: Real-Time Software Design Method Chapter 4. Overview of Real-Time Software Design Method for Embedded Systems Chapter 5. Structural Modeling for Real-Time Embedded Systems with SysML and UML Chapter 6. Use Case Modeling for Real-Time Embedded Systems Chapter 7. State Machines for Real-Time Embedded Systems Chapter 8. Object and Class Structuring for Real-Time Embedded Software Chapter 9. Dynamic Interaction Modeling for Real-Time Embedded Software Chapter 10. Software Architectures for Real-Time Embedded Systems Chapter 11. Software Architectural Patterns for Real-Time Embedded Systems Chapter 12. Component-Based Software Architectures for Real-Time Embedded Systems Chapter 13. Concurrent Real-Time Software Task Design Chapter 14. Detailed Real-Time Software Design Chapter 15. Designing Real-Time Software Product Line Architectures Part III: Analysis of Real-Time Software Designs Chapter 16. System and Software Quality Attributes for Real-Time Embedded Systems Chapter 17. Performance Analysis of Real-Time Software Designs Chapter 18. Applying Performance Analysis to Real-Time Software Designs Part IV: Real-Time Software Design Case Studies for Embedded Systems Chapter 19. Microwave Oven Control System Case Study Chapter 20. Railroad Crossing Control System Case Study Chapter 21. Light Rail Control System Case Study Chapter 22. Pump Control System Case Study Chapter 23. Highway Toll Control System Case Study Appendix A. Conventions Used in This Textbook Appendix B. Catalog of Software Architectural Patterns Appendix C. Pseudocode Templates for Concurrent Tasks Appendix D. Teaching Considerations
目录 历史 前言 I. C语言入门 1. 程序的基本概念 1. 程序和编程语言 2. 自然语言和形式语言 3. 程序的调试 4. 第一个程序 2. 常量、变量和表达式 1. 继续Hello World 2. 常量 3. 变量 4. 赋值 5. 表达式 6. 字符类型与字符编码 3. 简单函数 1. 数学函数 2. 自定义函数 3. 形参和实参 4. 全局变量、局部变量和作用域 4. 分支语句 1. if语句 2. if/else语句 3. 布尔代数 4. switch语句 5. 深入理解函数 1. return语句 2. 增量式开发 3. 递归 6. 循环语句 1. while语句 2. do/while语句 3. for语句 4. break和continue语句 5. 嵌套循环 6. goto语句和标号 7. 结构体 1. 复合类型与结构体 2. 数据抽象 3. 数据类型标志 4. 嵌套结构体 8. 数组 1. 数组的基本概念 2. 数组应用实例:统计随机数 3. 数组应用实例:直方图 4. 字符串 5. 多维数组 9. 编码风格 1. 缩进和空白 2. 注释 3. 标识符命名 4. 函数 5. indent工具 10. gdb 1. 单步执行和跟踪函数调用 2. 断点 3. 观察点 4. 段错误 11. 排序与查找 1. 算法的概念 2. 插入排序 3. 算法的时间复杂度分析 4. 归并排序 5. 线性查找 6. 折半查找 12. 栈与队列 1. 数据结构的概念 2. 堆栈 3. 深度优先搜索 4. 队列与广度优先搜索 5. 环形队列 13. 本阶段总结 II. C语言本质 14. 计算机数的表示 1. 为什么计算机用二进制计数 2. 不同进制之间的换算 3. 整数的加减运算 3.1. Sign and Magnitude表示法 3.2. 1's Complement表示法 3.3. 2's Complement表示法 3.4. 有符号数和无符号数 4. 浮点数 15. 数据类型详解 1. 整型 2. 浮点型 3. 类型转换 3.1. Integer Promotion 3.2. Usual Arithmetic Conversion 3.3. 由赋值产生的类型转换 3.4. 强制类型转换 3.5. 编译器如何处理类型转换 16. 运算符详解 1. 位运算 1.1. 按位与、或、异或、取反运算 1.2. 移位运算 1.3. 掩码 1.4. 异或运算的一些特性 2. 其它运算符 2.1. 复合赋值运算符 2.2. 条件运算符 2.3. 逗号运算符 2.4. sizeof运算符与typedef类型声明 3. Side Effect与Sequence Point 4. 运算符总结 17. 计算机体系结构基础 1. 内存与地址 2. CPU 3. 设备 4. MMU 5. Memory Hierarchy 18. x86汇编程序基础 1. 最简单的汇编程序 2. x86的寄存器 3. 第二个汇编程序 4. 寻址方式 5. ELF文件 5.1. 目标文件 5.2. 可执行文件 19. 汇编与C之间的关系 1. 函数调用 2. main函数和启动例程 3. 变量的存储布局 4. 结构体和联合体 5. C内联汇编 6. volatile限定符 20. 链接详解 1. 多目标文件的链接 2. 定义和声明 2.1. extern和static关键字 2.2. 头文件 2.3. 定义和声明的详细规则 3. 静态库 4. 共享库 4.1. 编译、链接、运行 4.2. 动态链接的过程 4.3. 共享库的命名惯例 5. 虚拟内存管理 21. 预处理 1. 预处理的步骤 2. 宏定义 2.1. 函数式宏定义 2.2. 内联函数 2.3. #、##运算符和可变参数 2.4. 宏展开的步骤 3. 条件预处理指示 4. 其它预处理特性 22. Makefile基础 1. 基本规则 2. 隐含规则和模式规则 3. 变量 4. 自动处理头文件的依赖关系 5. 常用的make命令行选项 23. 指针 1. 指针的基本概念 2. 指针类型的参数和返回值 3. 指针与数组 4. 指针与const限定符 5. 指针与结构体 6. 指向指针的指针与指针数组 7. 指向数组的指针与多维数组 8. 函数类型和函数指针类型 9. 不完全类型和复杂声明 24. 函数接口 1. 本章的预备知识 1.1. strcpy与strncpy 1.2. malloc与free 2. 传入参数与传出参数 3. 两层指针的参数 4. 返回值是指针的情况 5. 回调函数 6. 可变参数 25. C标准库 1. 字符串操作函数 1.1. 初始化字符串 1.2. 取字符串的长度 1.3. 拷贝字符串 1.4. 连接字符串 1.5. 比较字符串 1.6. 搜索字符串 1.7. 分割字符串 2. 标准I/O库函数 2.1. 文件的基本概念 2.2. fopen/fclose 2.3. stdin/stdout/stderr 2.4. errno与perror函数 2.5. 以字节为单位的I/O函数 2.6. 操作读写位置的函数 2.7. 以字符串为单位的I/O函数 2.8. 以记录为单位的I/O函数 2.9. 格式化I/O函数 2.10. C标准库的I/O缓冲区 2.11. 本节综合练习 3. 数值字符串转换函数 4. 分配内存的函数 26. 链表、二叉树和哈希表 1. 链表 1.1. 单链表 1.2. 双向链表 1.3. 静态链表 1.4. 本节综合练习 2. 二叉树 2.1. 二叉树的基本概念 2.2. 排序二叉树 3. 哈希表 27. 本阶段总结 III. Linux系统编程 28. 文件与I/O 1. 汇编程序的Hello world 2. C标准I/O库函数与Unbuffered I/O函数 3. open/close 4. read/write 5. lseek 6. fcntl 7. ioctl 8. mmap 29. 文件系统 1. 引言 2. ext2文件系统 2.1. 总体存储布局 2.2. 实例剖析 2.3. 数据块寻址 2.4. 文件和目录操作的系统函数 3. VFS 3.1. 内核数据结构 3.2. dup和dup2函数 30. 进程 1. 引言 2. 环境变量 3. 进程控制 3.1. fork函数 3.2. exec函数 3.3. wait和waitpid函数 4. 进程间通信 4.1. 管道 4.2. 其它IPC机制 5. 练习:实现简单的Shell 31. Shell脚本 1. Shell的历史 2. Shell如何执行命令 2.1. 执行交互式命令 2.2. 执行脚本 3. Shell的基本语法 3.1. 变量 3.2. 文件名代换(Globbing):* ? [] 3.3. 命令代换:`或 $() 3.4. 算术代换:$(()) 3.5. 转义字符\ 3.6. 单引号 3.7. 双引号 4. bash启动脚本 4.1. 作为交互登录Shell启动,或者使用--login参数启动 4.2. 以交互非登录Shell启动 4.3. 非交互启动 4.4. 以sh命令启动 5. Shell脚本语法 5.1. 条件测试:test [ 5.2. if/then/elif/else/fi 5.3. case/esac 5.4. for/do/done 5.5. while/do/done 5.6. 位置参数和特殊变量 5.7. 函数 6. Shell脚本的调试方法 32. 正则表达式 1. 引言 2. 基本语法 3. sed 4. awk 5. 练习:在C语言使用正则表达式 33. 信号 1. 信号的基本概念 2. 产生信号 2.1. 通过终端按键产生信号 2.2. 调用系统函数向进程发信号 2.3. 由软件条件产生信号 3. 阻塞信号 3.1. 信号在内核的表示 3.2. 信号集操作函数 3.3. sigprocmask 3.4. sigpending 4. 捕捉信号 4.1. 内核如何实现信号的捕捉 4.2. sigaction 4.3. pause 4.4. 可重入函数 4.5. sig_atomic_t类型与volatile限定符 4.6. 竞态条件与sigsuspend函数 4.7. 关于SIGCHLD信号 34. 终端、作业控制与守护进程 1. 终端 1.1. 终端的基本概念 1.2. 终端登录过程 1.3. 网络登录过程 2. 作业控制 2.1. Session与进程组 2.2. 与作业控制有关的信号 3. 守护进程 35. 线程 1. 线程的概念 2. 线程控制 2.1. 创建线程 2.2. 终止线程 3. 线程间同步 3.1. mutex 3.2. Condition Variable 3.3. Semaphore 3.4. 其它线程间同步机制 4. 编程练习 36. TCP/IP协议基础 1. TCP/IP协议栈与数据包封装 2. 以太网(RFC 894)帧格式 3. ARP数据报格式 4. IP数据报格式 5. IP地址与路由 6. UDP段格式 7. TCP协议 7.1. 段格式 7.2. 通讯时序 7.3. 流量控制 37. socket编程 1. 预备知识 1.1. 网络字节序 1.2. socket地址的数据类型及相关函数 2. 基于TCP协议的网络程序 2.1. 最简单的TCP网络程序 2.2. 错误处理与读写控制 2.3. 把client改为交互式输入 2.4. 使用fork并发处理多个client的请求 2.5. setsockopt 2.6. 使用select 3. 基于UDP协议的网络程序 4. UNIX Domain Socket IPC 5. 练习:实现简单的Web服务器 5.1. 基本HTTP协议 5.2. 执行CGI程序 A. 字符编码 1. ASCII码 2. Unicode和UTF-8 3. 在Linux C编程使用Unicode和UTF-8 B. GNU Free Documentation License Version 1.3, 3 November 2008 参考书目 索引
以下是STC12C4052AD单片机ADC采样的C语言代码示例: ```c #include <reg52.h> #define ADC_POWER 0x80 //ADC电源控制位 #define ADC_FLAG 0x10 //ADC完成标志位 #define ADC_START 0x08 //ADC开始转换控制位 sbit ADC_CS = P3^4; //模拟量输入通道选择端口 sbit ADC_CLK = P3^5; //ADC时钟端口 sbit ADC_DIN = P3^6; //模拟量输入端口 sbit ADC_DOUT = P3^7; //ADC数据输出端口 //ADC转换函数 int ADC_Convert(unsigned char ch) { unsigned char i; unsigned int dat = 0; ADC_CS = 1; //拉高通道选择端口,准备转换 ADC_CLK = 0; //ADC时钟置低 ADC_DIN = 0; //清零模拟量输入端口 //发送起始转换命令 ADC_CS = 0; ADC_CLK = 0; ADC_DIN = 1; ADC_CLK = 1; ADC_DIN = 1; ADC_CLK = 0; //发送通道选择命令 for (i = 0; i < 3; i++) { ADC_CLK = 0; ADC_DIN = ch & 0x80; ch <<= 1; ADC_CLK = 1; } //接收ADC转换结果 for (i = 0; i < 12; i++) { ADC_CLK = 0; ADC_CLK = 1; dat <<= 1; dat |= ADC_DOUT; } ADC_CS = 1; //转换完成,拉高通道选择端口 return dat; } void main() { unsigned int adc_data; P1 = 0x00; //初始化P1口为输出 P3 = 0xff; //初始化P3口为输入 while (1) { P1 = 0x00; //将P1口清零 adc_data = ADC_Convert(0); //读取ADC转换结果 P1 = adc_data >> 4; //将ADC转换结果输出到P1口 } } ``` 该代码实现了单通道ADC采样,并将转换结果通过P1口输出。其,ADC_Convert函数用于进行ADC转换,参数ch表示需要采样的模拟量输入通道,返回值为转换结果。在主函数,首先进行了端口初始化,然后进入循环,不断采样并输出结果。注意,ADC转换过程需要保证时序的正确性,因此需要仔细按照代码时序进行实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值