编译器
跳转到“编译器”attribute 是 GCC 编译器的扩展机制,用于为函数、变量、类型添加特殊属性,控制编译器的行为。
__attribute__ 是 GCC 编译器的扩展机制,用于为函数、变量、类型添加特殊属性,控制编译器的行为。
基本语法
__attribute__((属性1, 属性2, ...))
/* 双层括号是必须的! */__attribute__((section(".mydata"))) /* 正确 */__attribute__(section(".mydata")) /* 错误!缺少括号 */1. 变量声明
/* 位置1:类型之后,变量名之前(推荐) */int __attribute__((section(".mydata"))) var = 5;
/* 位置2:整个声明之前 */__attribute__((section(".mydata"))) int var = 5;
/* 位置3:变量名之后 */int var __attribute__((section(".mydata"))) = 5;
/* 多个属性 */uint32_t __attribute__((section(".dma_buf"), aligned(32))) dma_buffer[256];2. 函数声明
/* 位置1:返回类型之前 */__attribute__((section(".ramfunc"))) void func(void);
/* 位置2:返回类型之后 */void __attribute__((section(".ramfunc"))) func(void);
/* 位置3:函数名之后,参数列表之前 */void func(void) __attribute__((section(".ramfunc")));
/* 位置4:函数定义 */__attribute__((weak, alias("Default_Handler")))void USART1_IRQHandler(void);3. 结构体/类型定义
/* 结构体属性 */struct __attribute__((packed)) MyStruct { uint8_t a; uint32_t b;};
/* typedef 中使用 */typedef struct { uint8_t data[32];} __attribute__((aligned(4))) AlignedBuffer;常用属性详解
1. section - 指定存储段
/* 变量放入指定段 */uint32_t __attribute__((section(".ccmram"))) fast_var;uint8_t __attribute__((section(".dma_buf"))) dma_buffer[1024];
/* 函数放入指定段 */__attribute__((section(".itcm")))void critical_function(void) { // 代码放入ITCM RAM执行}
/* 只读数据放入指定段 */const uint32_t __attribute__((section(".rodata.custom")))lookup_table[] = {0x1234, 0x5678};2. aligned - 对齐方式
/* 32字节对齐(DMA常用) */uint8_t __attribute__((aligned(32))) dma_buffer[1024];
/* 结构体对齐 */struct __attribute__((aligned(16))) { uint32_t field1; uint32_t field2;} aligned_struct;
/* Cache line对齐(通常32字节) */__attribute__((aligned(32)))uint8_t cache_aligned_buffer[256];packed - 紧凑存储
/* 取消结构体对齐(节省空间) */struct __attribute__((packed)) { uint8_t a; // 1字节 uint32_t b; // 4字节 uint8_t c; // 1字节} compact; // 总共6字节(不是12字节)
/* 用于协议数据结构 */typedef struct __attribute__((packed)) { uint8_t header; uint16_t length; uint32_t data; uint8_t checksum;} ProtocolPacket; // 确保与协议规定完全一致weak - 弱符号
/* 弱函数(可被覆盖) */__attribute__((weak))void Default_Handler(void) { while(1);}
/* HAL库回调函数 */__attribute__((weak))void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { /* 用户可以重新定义此函数 */ UNUSED(huart);}used/unused - 使用标记
/* 防止被优化掉 */__attribute__((used))static const char version[] = "V1.0.0";
/* 消除未使用警告 */__attribute__((unused))static void debug_function(void) { // 仅在调试时使用}
/* 参数未使用 */void callback(__attribute__((unused)) void *param) { // param参数未使用,不会产生警告}** noinit - 不初始化**
/* 不在启动时初始化(节省启动时间) */__attribute__((section(".noinit")))uint8_t large_buffer[65536];
/* RAM保持数据 */__attribute__((section(".backup_sram"), noinit))uint32_t retain_data[256];interrupt - 中断处理
/* ARM Cortex-M中断处理 */__attribute__((interrupt("IRQ")))void USART1_IRQHandler(void) { // 编译器自动生成中断进入/退出代码}optimize - 优化控制
/* 特定函数优化级别 */__attribute__((optimize("O3")))void performance_critical(void) { // 最大优化}
__attribute__((optimize("O0")))void debug_function(void) { // 不优化,便于调试}always_inline/noinline
/* 强制内联 */__attribute__((always_inline))static inline void fast_function(void) { // 总是内联展开}
/* 禁止内联 */__attribute__((noinline))void no_inline_function(void) { // 永不内联}宏定义
跳转到“宏定义”| 后缀 | 含义 | 示例 |
|---|---|---|
u 或 U | 无符号(unsigned) | 100u |
l 或 L | 长整型(long) | 100L |
ul 或 UL | 无符号长整型(unsigned long) | 100UL |
ll 或 LL | 长长整型(long long) | 100LL |
ull 或 ULL | 无符号长长整型(unsigned long long) | 100ULL |
| 写法 | 类型 | 位数(通常) |
|---|---|---|
16 | int(有符号) | 16/32 位 |
16u | unsigned int | 16/32 位 |
16UL | unsigned long | 32 位 |
16ULL | unsigned long long | 64 位 |
重复宏定义问题
是的,() 确实会影响!这是一个重要的区别:
#define TMC4361A_RESET_LOW()- 这是一个宏函数,可以接受参数#define TMC4361A_RESET_LOW- 这是一个宏常量,不能接受参数
在你的代码中:
#define TMC4361A_RESET_LOW() HAL_GPIO_WritePin(TMC4361A_NRST_PORT, TMC4361A_NRST_PIN, GPIO_PIN_RESET)这意味着当你调用 TMC4361A_RESET_LOW() 时,它会被替换为:
HAL_GPIO_WritePin(TMC4361A_NRST_PORT, TMC4361A_NRST_PIN, GPIO_PIN_RESET)如果你之前有:
#define TMC4361A_RESET_LOW 1那么调用 TMC4361A_RESET_LOW() 就会变成 1(),这会导致编译错误,因为 1 不是一个函数。
所以删除常量定义是正确的解决方案,现在 TMC4361A_RESET_LOW() 可以正确地作为宏函数使用。
关键字
跳转到“关键字”extern
在C语言中使用这种方式(头文件中extern声明,源文件中定义)是一种标准做法,主要有以下原因:
1)避免重复定义错误:
- 如果在头文件中直接定义变量
TIM_HandleTypeDef htim2; - 当多个C文件包含该头文件时,会导致多次定义同一个变量
- 这会引发链接时的”multiple definition”错误
2)extern的作用:
extern关键字告诉编译器”这个变量在其他地方已定义,这里只是声明”- 声明只是告知编译器变量存在,但不分配内存空间
- 定义则会真正分配内存空间
3)一处定义规则:
- 在C语言中,全局变量应该只在一个源文件中定义
- 其他需要使用该变量的文件通过extern声明引用它
- 遵循”一次定义,多处声明”的原则
velotile
volatile 能做什么、不能做什么
- 能做:
- 禁止编译器把变量优化进寄存器
- 每次访问都“真正”读/写内存
- 不能做:
- 不保证原子性(比如 ++ 这种读-改-写不是原子的)
- 不解决并发竞态(ISR 和主循环同时改同一个变量仍可能出现竞争)
什么时候需要 volatile
- 中断服务函数里写、主循环里读的共享变量
- 例如你现在的
pb9_exti_triggered
- 例如你现在的
- 轮询外设寄存器(内存映射寄存器)
- 在 STM32 头文件里常见的
__IO宏其实就是volatile
- 在 STM32 头文件里常见的
- 被 DMA/外设/其他执行上下文修改的内存
中断里修改的变量,如果不声明为 volatile,编译器可能会认为主循环里它永远不会改变,从而优化掉检查!
// 外部中断回调:当配置为外部中断的引脚触发时由HAL调用void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){ if (GPIO_Pin == GPIO_PIN_9) { // 简单软件消抖:限制最小触发间隔,过滤机械抖动/毛刺 static uint32_t s_last_tick = 0; const uint32_t debounce_ms = 20; // 可按需要调整 10~30ms uint32_t now = HAL_GetTick(); if ((now - s_last_tick) >= debounce_ms) { pb9_exti_triggered = 1; s_last_tick = now; // 调试:在中断里翻转LED验证中断是否触发 HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_12); } }}
// main.c...extern volatile uint8_t pb9_exti_triggered;
while(1){ if(pb9_exti_triggered) { pb9_exti_triggered = 0; printf("PB9 EXTI triggered\n"); HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_10); }}编程注意项
跳转到“编程注意项”- 在C语言中,全局作用域(函数外部)只能进行声明和静态初始化,而不能执行运行时的赋值操作。这些uart = huart2;和tim = htim2;是运行时操作,必须放在函数内部执行。
// 全局声明 UART_HandleTypeDef *uart; // 错误:在全局域直接赋值操作 *uart = huart2; // 这行导致编译错误`- C语言头文件包含顺序问题笔记
在C语言中,头文件的包含顺序非常重要,这是因为编译器按顺序处理每个头文件。如果一个头文件使用了另一个头文件中定义的类型或宏,但没有包含那个头文件,就会产生”未知类型”的编译错误。
问题示例:
mb_m.h文件中使用了eMBErrorCode、UCHAR等类型- 但这些类型实际上是在
mb.h中定义的 mb_m.h自身没有包含mb.h- 解决方法:在包含
mb_m.h之前先包含mb.h
最佳实践:
- 头文件应自包含(包含它所依赖的所有头文件)
- 当遇到头文件依赖问题时,确保按正确顺序包含头文件
- 对于库开发者:使用头文件保护和前向声明避免这类问题
结构体
跳转到“结构体”基础
跳转到“基础”C99 引入的指定初始化器(designated initializer) 语法,用来按成员名而不是按顺序给结构体/联合体成员赋值。左边 .member = value 中的点号(.)就是“成员选择符”;未出现的成员会被自动置 0(或 NULL、0.0 等,视类型而定)。
/* 结构体声明 */typedef struct { int a; char b; double c;} Foo;
/* 指定初始化器 */Foo f = { .b = 'X', .c = 3.14, /* .a 未写出,自动置 0 */};运算符
跳转到“运算符”位运算
跳转到“位运算”示例
#define BIT_SET(reg, bit) ((reg) |= (1U << (bit)))#define BIT_CLEAR(reg, bit) ((reg) &= ~(1U << (bit)))#define BIT_TOGGLE(reg, bit) ((reg) ^= (1U << (bit)))#define BIT_READ(reg, bit) (((reg) >> (bit)) & 1U)新特性语法
跳转到“新特性语法”复合字面量”(compound literal) 语法
跳转到“复合字面量”(compound literal) 语法”C99 的“复合字面量”(compound literal) 语法,用来在表达式里临时创建一个指定类型的匿名对象。
(uint16_t[]){0x0001, 0x0000}- 类型:数组类型
uint16_t[2](用 [] 让编译器根据初始值个数自动推断长度) - 值:
{0x0001, 0x0000} - 作用域与生命周期:在当前代码块内具有自动存储期,直到离开所在的花括号作用域为止
- 用途:简洁地给需要
uint16_t*的参数传一小段常量数据
等价写法
- 写成显式的临时数组变量:
uint16_t tmp[] = {0x0001, 0x0000};mb_master_10h(2, 0x0064, 2, tmp);
- 或指定长度也行:
(uint16_t[2]){0x0001, 0x0000}
编程中的常见错误
跳转到“编程中的常见错误”| 维度 | 典型错误示例 | 嵌入式痛点级别 |
|---|---|---|
| 语法语义 | 未初始化、死代码 | ★☆☆ |
| 内存资源 | 越界、泄漏、双重释放、栈溢出 | ★★★ |
| 并发时序 | 竞态、死锁、优先级反转 | ★★★ |
| 编码规范 | MISRA/ CERT 违规 | ★★☆(认证强制) |
| 安全可靠性 | 输入未校验、缺省分支、未定义行为 | ★★★ |
| 可维护性 | 复杂度高、重复代码、魔术数 | ★★☆ |
这是静态检查的主要工作内容。
语法/语义级错误
跳转到“语法/语义级错误”内存/资源类
跳转到“内存/资源类”- 数组越界:
int buf[4]; buf[4] = 0; - 内存泄漏:early return 前忘了 free;OS 里动态申请的信号量没释放。
- 重复释放:中断和低优先级任务里 double-free 同一块 RAM。
- 栈爆炸:递归深度估算 300 字节,但 MCU 只给 256 字节,Coverity 会报 Stack overflow。
并发/时序
跳转到“并发/时序”- 竞态条件:全局标志位
g_ready在 IRQ 里写,主循环里读,未关中断。 - 死锁:任务 A 先拿
mutex1再拿mutex2,任务 B 顺序相反。 - 优先级反转:低优先级拿着信号量,被中优先级饿死,高优先级永久阻塞。
- 中断里调用
printf(不可重入)—— MISRA 直接禁止。
强规则/编码规范
跳转到“强规则/编码规范”安全/可靠性缺陷
跳转到“安全/可靠性缺陷”结构/可维护性问题
跳转到“结构/可维护性问题”来源:CSDN 原文,首次发布于 2025-07-27。