C语言_Misc(下)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

249.1.7C语言_Misc(下)

typedef enum                    // 定义一个枚举类型;以下枚举常量来自课程所查看的 STM32F1 HAL 头文件。
{                               // 枚举体开始。
  HAL_OK       = 0x00U,         // 操作成功,对应无符号整数常量 0。
  HAL_ERROR    = 0x01U,         // 一般错误,对应无符号整数常量 1。
  HAL_BUSY     = 0x02U,         // 外设或资源忙,对应无符号整数常量 2。
  HAL_TIMEOUT  = 0x03U          // 操作超时,对应无符号整数常量 3。
} HAL_StatusTypeDef;            // 把这个枚举类型命名为 HAL_StatusTypeDef。

union person                    // 视频中的共用体示例;四个成员从同一首地址开始,共享同一片存储空间。
{                               // 共用体定义开始。
    short age;                  // short 成员;在视频采用的目标 ABI 中占 2 字节。
    char name[2];               // 两个 char 元素,总计 2 字节,也可用于逐字节观察对象表示。
    int sex;                    // int 成员;在 STM32 工程中占 4 字节,并决定本例所需的最大成员空间和对齐。
    char i;                     // 单字节成员;写它时只覆盖共享区域中的一个字节。
};                              // 共用体类型定义结束;本例在当前 STM32 ABI 下通常为 4 字节。

一、大小端

字节序(endianness)描述一个多字节标量值的各个字节怎样映射到连续内存地址。它只讨论字节之间的排列,不改变每个字节内部各位的编号,也不影响单字节对象。

1.1 内存图示例

以 32 位值 0x12345678 为例,它由四个字节 0x12、0x34、0x56、0x78 组成。其中 0x12 是最高有效字节,0x78 是最低有效字节。假设对象首地址为 0x10:

内存地址大端存放小端存放
0x10(最低地址)0x120x78
0x110x340x56
0x120x560x34
0x13(最高地址)0x780x12
  • 大端(big-endian):最高有效字节放在最低地址,即“高字节在低地址”。
  • 小端(little-endian):最低有效字节放在最低地址,即“低字节在低地址”;等价地说,高字节在高地址。

“高字节”和“高地址”不是一回事:前者由这个数值中各字节的有效位权决定,后者由内存地址的数值大小决定。画图时地址朝上还是朝下只是排版选择,应始终看清地址标号。

1.2 大小端判断

课程中“单片机一般都是大端”的说法是错误的,不能按“单片机、电脑、手机”分类判断字节序。字节序由处理器实现、系统配置和 ABI 决定,同一处理器架构甚至可能支持两种模式。具体到本课程使用的 STM32F103:它采用 Cortex-M3,本器件的 AIRCR.ENDIANESS 位读值为 0,即数据采用小端。ST 的 PM0056 Cortex-M3 编程手册对此有明确说明。

课程工程的编译命令使用 -mcpu=cortex-m3 -mthumb,没有切换成大端的选项,也与上述芯片配置一致。常见 x86/x64 Windows 机器同样是小端,但这属于处理器和平台 ABI 的属性,不能说“Windows 操作系统决定了小端”。至于 8051,它本身是 8 位架构,多字节 C 类型怎样布局还取决于具体编译器 ABI;51 与 STM32 能正常通信并不能证明二者字节序相同。

判断当前平台时,可以令一个至少 16 位的无符号整数保存 0x1234,再通过指向其对象表示首字节的 unsigned char * 读取最低地址:

  • 首字节为 0x34:小端;
  • 首字节为 0x12:大端。

C 语言明确允许通过字符类型查看任意对象的字节表示,所以使用 unsigned char * 最直接。课程后面用共用体的字符数组成员读取同一存储区,在当前 GCC/STM32 环境中也能观察到相同结果,但跨平台代码还要留意读取非最后写入成员时的语言规则和编译器约定。

字节序只在“把多个字节解释为一个数”时产生影响。串口、CAN 或网络链路最终传输的都是按顺序排列的字节;接口本身不知道发送缓冲区中的四个字节原本是不是一个 uint32_t。如果双方传输 ASCII 字符串 "1234",接收顺序由协议确定,主机内存字节序通常无关;如果直接把整数的内存原样发送,才会把本机字节序带进协议。

视频猜测串口工具可能自动反转字节序,这也不能当作通用机制。普通串口终端一般按接收先后显示字节,不会自动猜测并反转任意多字节字段;某些工具的“按 16/32 位数值解析”功能可能让用户选择端序,但那是明确的显示规则。通信是否正确,必须查看协议规定和实际发送的字节序列。

1.3 字节序转换

字节序转换不是因为“32 位系统与 Windows 不同”,而是因为通信协议规定的线缆格式可能与本机表示不同。发送方应按协议逐字段序列化,接收方再按同一规则反序列化。互联网协议常把最高有效字节先传,称为网络字节序或大端;例如 RFC 4346 第 4.1 节明确给出了多字节数值从首字节到末字节的组合规则。

把 0x12345678 从一端序转换为另一端序,结果是 0x78563412,即交换第 1、4 字节和第 2、3 字节。实现时应使用 uint16_t、uint32_t 这类宽度明确的无符号类型,或使用平台提供的字节交换/主机序与网络序接口。对有符号数直接右移可能涉及实现定义行为;先转为相同宽度的无符号类型更清楚。

更稳妥的协议设计是不发送 C 结构体的原始内存。结构体可能含填充字节,字段宽度、对齐和字节序也可能随编译器与平台变化。应在协议中明确每个字段的位宽、符号、字节顺序和偏移,再逐字节编码和解码。

二、枚举

枚举把一组相关的整数值赋予有意义的名字。它不只是“让代码好看”,还形成一个独立的枚举类型,让函数参数、返回状态和调试信息更能表达业务含义。

2.1 示例代码

视频查看的是 STM32 HAL 的 HAL_StatusTypeDef:HAL_OK、HAL_ERROR、HAL_BUSY、HAL_TIMEOUT 分别显式指定为 0x00U 至 0x03U。HAL 函数用这个类型返回状态,调用者看到名字就能区分成功、一般错误、忙和超时。

枚举成员是整数常量,不是四个可修改的 int 变量。若第一个枚举成员没有显式赋值,它默认为 0;后续未赋值成员等于前一个成员加 1。显式赋值后,计数从该值继续,也允许不同成员拥有相同数值。

枚举类型的底层表示由 C 实现选择,只需能表示相应枚举值;在常见 ARM GCC 配置中通常采用 int 大小,但编译选项可能改变它。因此,在通信协议、Flash 文件格式或寄存器映射里需要固定宽度时,不能仅凭 enum 猜测占用字节数,应使用明确宽度的存储字段并在接口处转换。

把状态分别写成几个普通 int 变量并不等价:变量会占存储空间且可以被修改,枚举常量则是编译期整数常量。若只需要一组有名称的状态值,枚举更准确。

三、共用体

共用体(union,也叫联合体)的全部成员从同一地址开始,共享同一片存储。结构体为各成员分配互不重叠的区域,共用体则让不同类型成为同一组字节的不同视图。

共用体大小至少能容纳最大成员,并会按最严格成员的对齐要求向上取整,所以“大小永远等于最大成员”不是完整规则。本视频例子在当前 STM32 ABI 下,short 为 2 字节、char[2] 为 2 字节、int 为 4 字节,最大对齐为 4,因此 union person 通常为 4 字节。

若把相同成员列表改成结构体并保留末尾的 char i,典型布局是:age 占偏移 0~1,name 占 2~3,sex 占 4~7,i 占偏移 8,末尾再填充 3 字节以满足 4 字节对齐,总大小为 12 字节。删除 i 后通常为 8 字节。这里的具体数值仍属于当前 ABI 的结果,不能脱离目标平台硬编码。

写入一个共用体成员会改动共享区域中该成员覆盖的字节,但“每次赋值都会把先前成员完全清空”不准确。例如先写 4 字节的 sex,再写 2 字节的 age,通常只有共享区域中的前 2 字节被新值覆盖,另外 2 字节仍保留原来的位模式;只是不能再把整块存储简单理解为原先那个完整 sex 值。

共用体不会记录“当前有效的是哪个成员”。如果业务逻辑需要安全地区分类型,常见做法是额外保存一个枚举标签,读取前先根据标签决定使用哪个成员。这种“枚举标签 + 共用体载荷”的组合常用于协议消息、事件参数和状态机数据。

3.1 大小端的判断

视频让 age 保存 0x1234,再看 name[0]:由于所有成员首地址相同,name[0] 就对应对象的最低地址字节。在 STM32F103 的小端布局下,最低地址保存低字节,所以结果应为 0x34;若某个平台采用大端,结果才是 0x12。

需要区分两个概念:

  • 用字符数组观察另一个对象的组成字节,是为了检查对象表示;
  • 在业务中写入一个成员后把另一个不相关成员当作正常值使用,是类型重解释,可移植性和语义限制更复杂。

只为判断端序时,优先用 unsigned char * 查看无符号整数的首字节,含义最明确。协议收发则应直接按协议序列化,不应在每次运行时判断本机端序后盲目翻转;只有本机序与协议序不同时才转换。

四、实时操作系统

课程最后把实时操作系统概括为“用 POSIX 创建很多线程,每个线程执行一小段时间”。这只是很粗的类比。RT-Thread 的线程由内核线程接口创建,POSIX 兼容层是可选组件,不是调度器的基础。

RT-Thread 使用基于优先级的抢占式调度:高优先级就绪线程可以抢占低优先级线程;同优先级线程在时间片耗尽、主动让出或阻塞等条件下切换。线程等待延时、信号量或设备事件时会进入非就绪状态,CPU 随即运行其他就绪线程。因此,多线程效果来自调度、阻塞和中断共同协作,不能只理解为“所有线程轮流执行固定的一小段时间”。后续课程将分别讲动态/静态创建线程、线程状态和调度参数。

五、C语言知识总结

本阶段需要把知识串联起来:指针决定怎样访问对象,链表用指针组织不连续数据,存储期和链接属性决定对象何时存在及名字在哪里可见,内存布局解释代码与数据最终位于何处,而字节序、枚举、共用体则直接影响协议和驱动接口的表达。

学习时不要只背“大端高字节在低地址”等口令。至少应能完成三件事:根据地址表画出 0x12345678 的两种布局;解释为什么串口发送整数原始内存有跨平台风险;说明枚举标签与共用体载荷怎样组成一种可判别的数据结构。做到这些,才算能把概念用于嵌入式代码。