Skip to content

LV010-嵌入式C语言

其实之前就学习过C预言了,语法什么的在嵌入式中都是一模一样的,就是有些数据类型还有相关操作可以再熟悉一下。

一、STM32支持的数据类型

以STM32F103ZE这一款芯片为例,这是一块32bit的MCU,基本数据类型在此款芯片中的数据长度,以及在HAL库函数中的定义( stdint.h文件中的定义,采用C99标准)如下图:

image-20230418221356751

建议在开发过程中使用库定义的数据类型, 来定义变量或函数, 比如unsigned char a, 使用uint8_t a。

二、位运算

1. 运算符

这个操作我们在对STM32进行编程的时候会将常用到。位运算是指二进制位之间的运算。在嵌入式系统设计中, 常常要处理二进制的问题,例如将某个寄存器中的某一个位置1或者置0, 将数据左移5位等。常用的位运算符如表 下表:

序号运算符含义序号运算符含义
(1)&按位与(2)|按位或
(3)~按位取反(4)<<左移
(5)>>右移(6)^按位异或
  • (1)按位与运算符( & )

参与运算的两个操作数,每个二进制位进行“与” 运算,若两个都为1,结果为1,否者为0。

c
1011 & 1001 = 1001

第一位都为1,结果为1;第二位都为0,结果为0;第三位一个为1,一个为0,结果为0;第四位都为1,结果为1。 最后结果为1001。

  • (2)按位或运算符( | )

参与运算的两个操作数,每个二进制位进行“ 或”运算,若两个都为0,结果为1,否者为1。

c
1011 | 1001 = 1011

第一位都为1,结果为1;第二位都为0,结果为0;第三位一个为1,一个为0,结果为1;第四位都为1,结果为1。最后结果为1011。

  • (3)按位取反运算符( ~ )

按位取反运算符用于对一个二进制数按位取反。

c
~1011 = 0100

第一位为1, 取反为0;第二位为0, 取反为1;第三位为1, 取反为0,结果为1;第四位为1, 取反为0。最后结果为0100。

  • (4)(5)左移( << )和右移( >> ) 运算符

左移( << )运算符用于将一个数左移若干位,右移( >> )运算符用于将一个数右移若干位。

c
unsigned char val = 1011 1001
val << 3 = 1011 1001 << 3 = 1100 1000
val >> 3 = 1011 1001 >> 3 = 0001 0111

若val=val<<3,表示val左移3位,然后赋值给val,左移过程中,高位移出去后被丢弃, 低位补0,最后val结果为11001000;若val=val>>3,表示val右移3位,然后赋值给val, 右移过程中, 低位移出去后被丢弃, 高位补0,最后val结果为00010111。

  • (6)按位异或运算符

简单来说就是相同为1不同为0,经过三次异或可以交换两个数的值。

c
a = 0x3 = 0011
b = 0x5 = 0110
    
a = a ^ b = 0011 ^ 0110 = 1010 = 0xa;
b = a ^ b = 1010 ^ 0110 = 0011 = 0x3;
a = a ^ b = 1010 ^ 0011 = 0110 = 0x5;

2. 清0或置1

在嵌入式中,经常使用位运算符实现清0或置1。

例如, MCU的ODR寄存器控制引脚的输出电平高低,寄存器为32位, 每位控制一个引脚的电平。假设需要控制GPIOB的1号引脚输出电平的高低,设置该寄存器第0位为1,输出高电平,设置该寄存器第0位为0,输出低电平。

c
#define GPIOB_ODR (*(volatile unsigned int *)(0x40010C0C))
GPIOB_ODR &= ~(1<<0);
GPIOB_ODR |= (1<<0);

第1行: 使用#define定义了GPIOB_ODR 对应的内存地址为0x40010C0C。 该地址为MCU的ODR寄存器地址。

第2行: GPIOB_ODR &= ~(1<<0)实际是GPIOB_ODR = GPIOB_ODR & ~(1<<0), 先将GPIOB_ODR和~(1<<0)的进行与运算,运算结果赋值给GPIOB_ODR。 1<<0 的值为 00000000 00000000 00000000 00000001, 再取反为11111111 11111111 11111111 11111110, 则GPIO_ODR的第0位和0与运算, 结果必为0,其它位和1 运算,由GPIO_ODR原来的值决定结果。这就实现了,只将GPIO_ODR的第0位清0,其它位保持不变的效果,实现了单独控制对应引脚电平输出低。

第3行: GPIOB_ODR |= (1<<0)实际是GPIOB_ODR = GPIOB_ODR | (1<<0),先将GPIOB_ODR和(1<<0)的进行或运算,运算结果赋值给GPIOB_ODR。 1<<0的值为00000000 00000000 00000000 00000001,则GPIO_ODR的第0位和0或运算,结果必为1,其它位和0运算,由GPIO_ODR原来的值决定结果。这就实现了,只将GPIO_ODR的第0位置1,其它位保持不变的效果,实现了单独控制对应引脚电平输出高。

三、非的使用

这个我们在编程的过程中也经常用到,非的结果只有两种,就是0和非0.

c
!0   ==> 1
!1   ==> 0
!100 ==> 0

四、__weak与__attribute__((weak))

为什么要了解这个?因为后边要用HAL库进行开发,HAL库里边好多好多这种符号,这里还是要了解一下的。前边学习C语言的时候,有专门学习过 __attribute__,具体可以看《LV01-17-C语言-attribute机制》。注意这一小节的笔记是后边补充过来的,由于MDK可以很好的看到代码生成的汇编,调试的时候可以看到代码和汇编的对应关系,所以这里用了后边创建的HAL库空工程。注意把工程的调试方式改成软件仿真,不改好像也没啥问题,不影响:

image-20230501164207687

1. 两者关系

GNU 的编译器(gcc)扩展了一个关键字 __attribute__,通过该关键字,用户可以在声明时指定特殊的属性,使用时该关键字后跟双括号内的属性,例如:__attribute__((属性名字))。属性名字都是定义好的,weak 属性就是其中之一:__attribute__((weak))。

在 ARM 编译器(armcc)中,支持和 GCC 相同的关键字 __attribute__,使用方式也基本相同,如下:

c
__attribute__((attribute1, attribute2, ...))            // 例如:void * Function_Attributes_malloc_0(int b) __attribute__((malloc));
__attribute__((__attribute1__, __attribute2__, ...))    // 例如:static int b __attribute__((__unused__));

除此之外,ARM 编译器(armcc)还扩展了一个关键字 __weak,例如:__weak void f(void); 或者 __weak int i;。ARM 的汇编器(armasm)以另一种方式 [WEAK] 支持该特性。在许多源码中,经常通过宏定义的形式来定义关键字,例如 上面linux 中的 __weak 就是 宏定义的 __attribute__((weak))

2. 强弱符号

在 GCC 中,被 __attribute__((weak)) 修饰的符号,称之为 弱符号(Weak Symbol)。例如:弱函数、弱变量;没有 __attribute__((weak)) 修饰的符号被称为强符号。在 ARM 中,没有弱符号和强符号这种叫法,只有个弱引用(Weak References) 和 非弱引用(non-weak reference ) 、 弱定义(Weak definitions) 和 非弱定义(non-weak definition)。需要注意的是编译器和汇编器都可以输出弱符号。

  • 非弱引用

非弱引用就是我们平常使用的对于非弱函数或者弱变量的引用。如果链接器无法在到目前为止已加载内容中解析对正常非弱符号的引用问题,则 它会尝试通过在库中找到符号 来解决此问题:如果找不到此类引用,则链接器将报告错误。如果解析了这样的引用,则从入口点可以通过至少一个非弱引用来访问的节区被标记为已使用。这样可以确保链接器不会将该节作为未使用的节删除。 每个非弱引用都必须通过一个定义来解决。 如果有多个定义,则链接器将报告错误。

  • 弱引用

引用弱声明的函数或者变量的引用即为弱引用。 链接器不会从库中加载对象来解析弱引用。仅当由于其他原因在镜像中包含了定义时,它才能解析弱引用。弱引用不会导致链接器将包含定义的节区标记为已使用,因此链接器可能会将其标记为未使用而删除。

3. __weak可以用在哪?

__weak 关键字可以应用于函数和变量的声明以及函数定义。

3.1 声明

__weak 可以用于函数声明或者变量的声明。对于声明,此存储类指定一个 extern 对象声明,即使该对象不存在,对于该声明的引用也不会导致链接器对未解析的引用(找不到定义的引用)当做错误来处理。

如果在当前编译单元中可以找到 __weak 声明定义,则会用找到的定义替换 __weak 引用;对于找不到定义 __weak 的声明(函数或变量),编译器做如下处理:

  • 引用被解析为分支连接指令 BL,找不到定义 __weak 的声明(函数或变量)等效于将被引用的分支为 NOP。

  • 最后直接将引用替换为 NOP 指令。

注意:必须是在当前编译单元,不再当前编译单元的没有意义(例如 func1 在 main.c 中只有__weak 声明,但是没有定义)。具体看下图的测试代码(这里):

image-20230501170931048

如上图所示,func1使用了__weak定义,也使用了__weak声明,并且在 main.c 中重写了,相当于在main.c中重新进行了定义,那么就会用重新进行的定义来替换__weak引用,所以引用被解析为BL,被引用分支就是func2函数,所以最终会跳转到main.c中定义的func2函数执行,,而func1并未使用__weak定义,但是月使用了__weak声明,然后在main.c中也并未重定义,所以直接被替换为NOP了。

【注意】我经过测试得出的结论(MDK v5.29.0):

(1)__weak声明函数,若定义时使用了__weak来修饰,那么这个函数就是一个弱函数,可以被重写,并且实际运行时不会被调用;但是定义时若是没有使用__weak,此时函数会被识别为弱函数,但是,无法重写,重写的话会报重定义错误,并且实际运行时也不会被运行。

(2)__weak定义函数,若使用__weak来修饰声明函数,那么这个函数就是一个弱函数,可以被重写,实际运行时此弱函数不会被调用;若是没有使用__weak来修饰声明函数,那么这个函数会被识别成非弱函数,但是,此函数依然可以重写,若是重写了这个函数,具体最终调用的我测过了,是重写的那一部分代码,若是没有重写,那么就会调用__weak定义的那一部分代码。

3.2 定义

用 __weak 定义的函数弱输出其符号。弱定义的函数的行为类似于正常定义的函数,除非将同名的非弱定义的函数链接到同一镜像中。 如果在同一镜像中同时存在非弱定义函数和弱定义函数,则对该函数的所有调用都会解析为调用非弱函数,否则直接使用弱定义的函数(与上面的若声明不同)。

上边什么意思呢?简单来说意思就是,我们的工程中,存在一个弱定义函数,我们要是没有重定义这个函数,那么链接的时候会使用弱定义的函数,若是重写了弱定义的函数,那么我们重写的非弱定义函数就会覆盖掉弱定义的函数,HAL库中,我们重写一些函数也是这个道理。

如果想要使用多个弱定义,则除非使用链接器选项 --muldefweak,否则链接器会生成一条错误消息。在这种情况下,链接器随机选择一个供所有调用来使用。使用方式如下:

c
//=============================================
/* weak_test.h !!!注意所在文件不同!!! */
void func1(void);
void func2(void);
//=============================================
/* weak_test.c !!!注意所在文件不同!!! */
void func1(void)
{
    func2();        /* 这里将替换为 main.c 中的 func2 */
}

__weak void func2(void)     /* 弱定义 */
{

}
//=============================================
/* main.c !!!注意所在文件不同!!! */
void func2(void)
{

}

int main (void)
{
	func2();
}

4. __weak使用的限制

(1)函数或变量不能在同一编译中同时弱和非弱地使用。

c
void func(void);
void g()
{
    func();    /* 非弱函数引用 */
}

__weak void func(void);
void h()
{
    func();    /* 弱函数引用 */
}

(2)不能在定义函数或变量的同一编译中使用弱函数或弱变量,如下将导致编译错误(正确的使用方式参考上面的使用示例)

c
/* weak_test.c 如下同一文件中的定义及使用将报错 */
__weak void func(void);

void h()
{
    func();
}

void func()
{

}

(3)弱函数不能是内联函数

5. __attribute__((weak))用在哪?

__attribute__关键字使您可以指定变量或结构字段,函数和类型的特殊属性(与具体属性)。该关键字的作用与 __weak 的作用基本是一样的,在使用时有些不同,此外在某些情况下,编译的处理也有些区别。

5.1 声明

这个参数是 GUN 编译器的一个扩展,ARM 编译器也支持该关键字。__attribute__((weak)) 可以声明弱变量,并且其声明方式与 __weak 相比更加灵活。除了 __weak 的声明方式,我们还可以用

c
extern int Variable_Attributes_weak_1 __attribute__((weak));

__attribute__((weak)) 可以声明弱函数,其声明方式与 __weak 相比更加灵活。除了 __weak 的声明方式,我们还可以用:

c
extern int Function_Attributes_weak_0 (int b) __attribute__((weak));

任何包含了 __attribute__((weak)); 声明的文件的中的同名函数定义,都将被当做弱函数。

image-20230501174643348

5.2 定义

用 __attribute__((weak))定义的函数弱输出其符号(与 __weak相同)。其使用方式有以下两种:

c
__attribute__((weak)) void func1(void)
{
    printf("Weak func1!\r\n");
}
/* 或者 */
void __attribute__((weak)) func1(void)
{
    printf("Weak func1!\r\n");
}

6. 两者的区别

网上找到的区别如下,但是我对这两个区别持怀疑态度,因为在前边__weak的声明一小节的例子中,显然使用func1并未使用__weak定义,但是使用了__weak来声明,最终也被识别为弱函数了:

(1)__weak 和 __attribute__((weak)) 在声明和定义的时候,其所处的位置有不同。

(2)__weak 仅在函数定义中使用时才会生成弱函数。而在任何情况下(声明和定义) __attribute__((weak)) 都会生成弱函数,无论是用于函数定义还是用于函数声明中!