嵌入式C编程:库管理、符号处理与实用工具
1. 库文件的创建与链接
1.1 ranlib的作用
在创建库文件
libsquare.a
时,需要指定其组成部分,例如:
libsquare.a(square_unsigned.o)
ranlib libsquare.a
前两行告知
make
命令
libsquare.a
由哪些组件构成,第三行
ranlib libsquare.a
则是在安装所有组件后,对归档文件运行
ranlib
程序,为归档文件创建目录表。
早期的链接器存在一些问题,例如当程序需要
b.o
中的函数,但
b.o
又依赖
a.o
中的函数时,链接器按顺序查找归档文件,找到
b.o
后不会再回头查找
a.o
,可能导致链接失败。为解决这个问题,引入了
ranlib
,它为归档文件添加目录表,使组件可以随机加载。
加载组件的新算法如下:
1. 查看未定义符号列表(如程序使用
b_funct
,则该符号未定义)。
2. 打开归档文件。
3. 检查目录表中是否有未定义的符号,若有则加载。
4. 重复上述步骤,直到该库无法满足更多符号需求。
1.2 链接库与程序
使用以下命令将库与程序进行链接:
square: square.o libsquare.a
$(CC) $(CFLAGS) -o square square.o -L. -lsquare
-L.
标志告诉链接器在当前目录查找库文件,
-lsquare
指定要链接的库为
libsquare.a
。链接器会先在当前目录查找,然后在系统目录查找。
1.3 完整的makefile示例
CFLAGS=-g -Wall -Wextra
all: square
square: square.o libsquare.a
$(CC) $(CFLAGS) -o square square.o -L. -lsquare
libsquare.a: libsquare.a(square_int.o) \
libsquare.a(square_float.o)
libsquare.a(square_unsigned.o)
ranlib libsquare.a
libsquare.a(square_int.o): square_int.o
ar crU libsquare.a square_int.o
libsquare.a(square_float.o): square_float.o
ar crU libsquare.a square_float.o
libsquare.a(square_unsigned.o): square_unsigned.o
ar crU libsquare.a square_unsigned.o
square_int.o: square_int.h square_int.c
square_float.o: square_float.h square_float.c
square_unsigned.o: square_unsigned.h square_unsigned.c
square.o: square_float.h square.h square_int.h
square_unsigned.h square.c
由于测试程序未调用
square_unsigned
,
square_unsigned.o
模块不会被链接到程序中,这展示了链接器不会链接不需要的目标文件。
1.4 确定性与非确定性库
理想情况下,运行
make
命令生成的二进制文件应始终相同。最初,库文件不存储组件的创建者和创建时间信息,但这给
make
程序带来了困难,因为它无法判断归档文件中的版本与新编译版本的新旧。
ar
命令经过修改,可以存储这些信息,但为了不破坏旧功能,存储信息变为可选。指定
D
选项时,不存储修改时间,生成确定性归档文件;指定
U
选项时,每次生成不同的二进制文件,但更受
make
程序青睐,默认使用
D
(旧格式)。
2. 弱符号的使用
2.1 弱符号的概念
在C语言中,GCC和大多数编译器支持弱符号。弱符号告诉链接器:“如果没有其他人定义这个符号,就使用我”。
2.2 STM中断表中的应用
在STM中断表中,必须为每个可能的中断定义函数,但有些中断可能永远不会发生。STM固件为所有中断定义了中断处理程序,若这些处理程序被调用,系统会停止,方便使用调试器排查问题。
例如,USART2中断的处理程序定义如下:
void USART2_IRQHandler(void) {
while(true)
continue;
}
如果我们自己定义了该处理程序,固件库中的版本将被忽略。
2.3 代码示例
以下是使用弱符号的代码示例:
main.c
#include "sub.h"
int main()
{
sub1();
sub2();
return (0);
}
sub.c
#include "sub.h"
void sub2(void) __attribute__((weak));
void sub1(void) {}
void sub2(void) {}
sub.h
#ifndef __SUB_H__
#define __SUB_H__
extern void sub1(void);
extern void sub2(void);
#endif // __SUB_H__
sub2.c
#include <stdio.h>
#include "sub.h"
void sub2(void) {
printf("The non-weak sub2\n");
}
如果链接
main.c
和
sub.c
,将链接弱定义的
sub2
;如果链接
main.c
、
sub.c
和
sub2.c
,则使用
sub2.c
中定义的非弱版本的
sub2
。
弱符号在中断例程等场景中非常有用,允许提供备用或默认版本。
3. 编程问题
3.1 几何形状面积计算库
编写一个库来计算几何形状的面积(如矩形面积、三角形面积等),每个函数应放在单独的目标文件中,并将所有面积函数组合到一个库中。同时编写一个主程序对这些函数进行单元测试。
3.2 串口输出程序重构
重写之前章节创建的串口输出程序,将所有与UART相关的代码放在一个单独的模块中。
3.3 弱符号测试
测试以下三种情况:
1. 定义两个弱符号和一个强符号。
2. 定义两个弱符号,无强符号。
3. 定义两个强符号。
4. 专业编程的建议
4.1 学习写作
专业程序员最重要的技能之一是良好的写作能力,编程过程中需要撰写多种文档,如提案、需求文档、设计文档、用户文档和销售资料等。可以参加创意写作课程,接受他人的反馈。
4.2 学习阅读
通过“阅读”技术文档可以学到很多知识,但这里的“阅读”并非指通读全文,而是扫描文档以找到所需信息。例如,在准备过程中可能会用到ISO/IEC9899:2017、RM0360参考手册等大量文档,但通常只使用其中的一小部分。可以熟练使用
find
命令定位感兴趣的文档部分。
4.3 协作与借鉴
与他人合作并相互审查工作,可以扩展自己和他人的知识。例如Linux内核的开发,数千人共享代码、接受反馈、改进代码,最终形成了强大的操作系统。查看公开可用的代码,了解其他程序员的做法也很有帮助,如STM32 Workbench附带的HAL固件。
5. 实用开源工具
5.1 Cppcheck
Cppcheck
是一个静态程序检查器,用于查找编译器未发现的程序问题。它能找出一些有趣的代码问题,例如:
if (flag) {
//(removed) processMessage()
} else {
//(removed) processMessage()
}
Cppcheck
产生的误报较少,如果它标记了代码中的某一行,建议仔细检查。
5.2 Doxygen
Doxygen
通过程序中的结构化注释为程序创建文档,但实际使用中存在一些问题,例如需要在代码中添加注释,且生成的文档看起来像是自动生成的。
Cppcheck
使用
Doxygen
进行内部文档编写。
5.3 Valgrind
Valgrind
是一套用于动态检查程序错误的工具,其中最常用的
memcheck
工具可以查找内存错误,如数组越界、释放后使用指针等,能有效检测大多数简单的运行时错误。
5.4 SQLite
SQLite
是一个适用于小型数据库(最多100,000条记录)的嵌入式数据库库,虽然不是最快的数据库,但对于不希望有大型数据库开销的小型嵌入式系统来说很适用。使用它需要学习SQL语言。
6. 项目创建清单
6.1 原生C项目
如果要创建在PC上运行的程序,可按以下步骤操作:
1. 启动STM32 Workbench,必要时选择工作空间。
2. 在主窗口中,选择
File ▶New ▶C/C++ Project
。
3. 在
C/C++ Project
窗口中,选择
C Managed Build
并点击
Next
。
4. 在
C Project
窗口中:
- 填写项目名称(无空格和特殊字符)。
- 在
Project Type
列中,选择
Executable ▶Empty Project
。
- 在
Toolchains
列中,选择适合本地系统的工具链,如Windows上的Visual C++,Linux上的GNU gcc。
- 点击
Next
。
5. 在
Configurations
窗口中:
- 取消勾选
Release
。
- 保持
Debug
勾选。
- 点击
Finish
。
6. 在主窗口中:
- 在
Project Explorer
(左列)中选择项目。
- 选择
File ▶New ▶Source File
。
- 在
Source File
窗口中:
- 在
Source File
字段中输入程序文件名(以
.c
结尾)。
- 点击
Finish
。
7. 在主窗口(文件显示在编辑面板中):
- 输入程序代码。
- 选择
File ▶Save All
。
- 选择
Project ▶Build Project
。
- 选择
Run ▶Run Configuration
。
8. 在
Create, Manage, and Run Configurations
窗口中:
- 在左列中选择
C/C++ Application
。
- 点击
New
。
- 在
C/C++ Application
下点击
Browse
。
- 在文件对话框中:
- 进入工作空间并打开项目文件夹。
- 在项目文件夹中打开
Debug
目录。
- 选择程序的可执行文件。
- 点击
OK
。
9. 在
Create, Manage, and Run Configurations
窗口中:
- 点击
Apply
。
- 点击
Close
。
此时可以运行和调试程序。
6.2 STM32 Workbench嵌入式项目
如果要创建在Nucleo板上运行的程序,可按以下步骤操作:
1. 启动STM32 Workbench,必要时选择工作空间。
2. 在主窗口中,选择
File ▶New ▶C Project
。
3. 在
C Project
窗口中:
- 填写项目名称(无空格和特殊字符)。
- 在
Project Type
列中,选择
Executable ▶Empty Project
。
- 在
Toolchains
列中,选择
Ac6 STM32 MCU GCC
。
- 点击
Next
。
4. 在
Configurations
窗口中:
- 取消勾选
Release
。
- 保持
Debug
勾选。
- 点击
Finish
。
5. 在
Target Configuration
窗口中:
- 选择
STM32F0
系列。
- 选择
NUCLEO-F030R8
板。
- 点击
Next
。
6. 在
Project Firmware Configuration
窗口中:
- 点击
Hardware Abstraction Layer (Cube HAL)
。
- 下载目标固件(仅需一次)。
- 点击
Finish
。
7. 在主窗口中:
- 在
Project Explorer
(左列)中选择项目。
- 选择
Project ▶Properties
。
8. 在
Properties
窗口中:
- 展开
C++ Build
。
- 进入
Settings
。
- 进入
Tool Settings
选项卡。
- 选择
MCU GCC Compiler ▶Debugging
,将调试级别设置为
Maximum (-g3)
。
- 选择
MCU GCC Compiler ▶Miscellaneous
,设置其他标志为
-fmessage-length=0 0Wa,adhls=$(@:%.o=%.lst)
,开启列表功能。
- 点击
Apply
。
- 点击
OK
。
9. 在主窗口中:
- 在
Project Explorer
(左列)中选择项目。
- 展开项目。
- 展开
src
目录。
- 点击
main.c
进行编辑。
- 输入程序代码。
- 选择
File ▶Save All
。
- 选择
Project ▶Build Project
。
- 选择
Project ▶Run
或
Project ▶Debug
。
总之,嵌入式C编程涉及库管理、符号处理、实用工具使用等多个方面,同时不断学习和实践是提升编程能力的关键。技术在不断发展,我们应保持学习的热情,探索更多的可能性。
7. 嵌入式C编程要点总结
7.1 库管理要点
-
库创建与链接
:通过
ar命令将目标文件打包成静态库,如ar crU libsquare.a square_int.o,再使用ranlib为库创建目录表以解决链接顺序问题。链接时使用-L指定库搜索路径,-l指定库名。 -
确定性与非确定性
:
ar命令的D和U选项可控制库的确定性,默认D选项生成确定性归档文件。
7.2 符号处理要点
- 弱符号应用 :弱符号允许在没有其他定义时使用自身,在STM中断表等场景中可提供备用或默认实现,避免重复定义错误。
7.3 实用工具总结
| 工具名称 | 功能描述 |
|---|---|
| Cppcheck | 静态程序检查器,查找编译器未发现的问题,误报少 |
| Doxygen | 通过结构化注释生成程序文档 |
| Valgrind |
动态检查程序错误,
memcheck
可检测内存错误
|
| SQLite | 适用于小型嵌入式系统的数据库库,需学习SQL语言 |
7.4 项目创建流程对比
| 项目类型 | 创建步骤关键差异 |
|---|---|
| 原生C项目 | 选择适合本地系统的工具链,在PC上运行 |
| STM32 Workbench嵌入式项目 |
选择
Ac6 STM32 MCU GCC
工具链,配置目标板和固件,在Nucleo板上运行
|
8. 未来学习方向与展望
8.1 持续学习新技术
计算机技术发展迅速,嵌入式领域不断有新的芯片、框架和工具出现。例如,随着物联网的发展,对低功耗、高性能嵌入式设备的需求增加,我们需要学习新的通信协议(如LoRa、ZigBee)和低功耗设计技术。
8.2 深入研究现有技术
对于已经掌握的技术,如库管理、弱符号使用等,可以进一步深入研究其底层原理。例如,了解
ar
和
ranlib
命令的具体实现,以及链接器在处理弱符号和强符号时的详细算法。
8.3 参与开源项目
参与开源项目是提升技术能力和扩展人脉的好方法。可以从简单的贡献开始,如修复代码中的小错误、完善文档等,逐渐参与到项目的核心开发中。例如,参与Linux内核或STM32相关的开源项目。
8.4 实践项目积累经验
通过实践项目将所学知识应用到实际中,不断积累经验。可以从简单的小项目开始,如制作一个简单的传感器数据采集系统,逐渐挑战更复杂的项目,如开发一个完整的嵌入式操作系统。
9. 总结与建议
9.1 总结
嵌入式C编程是一个综合性的领域,涉及库管理、符号处理、实用工具使用和项目创建等多个方面。掌握这些知识和技能,能够帮助我们更高效地开发嵌入式系统。
9.2 建议
- 多实践 :通过实际项目来巩固所学知识,不断尝试新的技术和方法。
- 学习交流 :加入技术社区,与其他开发者交流经验,分享学习心得。
- 持续学习 :关注行业动态,不断学习新的知识和技能,适应技术的快速发展。
以下是一个简单的mermaid流程图,展示嵌入式C编程学习的基本流程:
graph LR
A[学习基础知识] --> B[实践项目]
B --> C[参与开源项目]
C --> D[深入研究技术]
D --> E[持续学习新技术]
E --> B
总之,嵌入式C编程是一个充满挑战和机遇的领域,只要我们保持学习的热情,不断实践和探索,就能够在这个领域取得不错的成绩。
超级会员免费看

被折叠的 条评论
为什么被折叠?



