历史上的今天
返回首页

历史上的今天

今天是:2025年08月12日(星期二)

正在发生

2021年08月12日 | STM32 | STM32中一些非常重要的C语言知识点汇总

2021-08-12 来源:eefocus

说在前面的话

一位初学单片机的小伙伴让我推荐C语言书籍,因为C语言基础比较差,想把C语言重新学一遍,再去学单片机,我以前刚学单片机的时候也有这样子的想法。


其实C语言是可以边学单片机边学的,学单片机的一些例程中,遇到不懂的C语言知识,再去查相关的知识点,这样印象才会深刻些。


下面就列出了一些STM32中重要的C语言知识点,初学的小伙伴可以多读几遍,其中大多知识点之前都有写过,这里重新整理一下,更详细地分析解释可以阅读附带的链接。


assert_param

断言(assert)就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。


断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真。


可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言,而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。

注意assert()是一个宏,而不是函数。

在STM32中,常常会看到类似代码:


assert_param(IS_ADC_ALL_INSTANCE(hadc->Instance));

assert_param(IS_ADC_SINGLE_DIFFERENTIAL(SingleDiff));


这是用来检查函数传入的参数的有效性。STM32中的assert_param默认是不使用的,即:

如果要使用,需要定义USE_FULL_ASSERT宏,并且需要自己实现assert_failed函数。特别的,使用STM32CubeMX生成代码的话,会在main.c生成:

我们在这进行填充就好。


下面分享一下assert的应用例子:

#include #include  

int main(void)

{

 int a, b, c;

 printf("请输入b, c的值:");

 scanf("%d %d", &b, &c);

 a = b / c;

 printf("a = %d", a);

 return 0;

}


此处,变量c作为分母是不能等于0,如果我们输入2 0,结果是什么呢?结果是程序会蹦:

这个例子中只有几行代码,我们很快就可以找到程序蹦的原因就是变量c的值为0。但是,如果代码量很大,我们还能这么快的找到问题点吗?

这时候,assert()就派上用场了,以上代码中,我们可以在a = b / c;这句代码之前加上assert(c);这句代码用来判断变量c的有效性。此时,再编译运行,得到的结果为:

可见,程序蹦的同时还会在标准错误流中打印一条错误信息:

Assertion failed:c, file hello.c, line 12

这条信息包含了一些对我们查找bug很有帮助的信息:问题出在变量c,在hello.c文件的第12行。这么一来,我们就可以迅速的定位到问题点了。


这时候细心的朋友会发现,上边我们对assert()的介绍中,有这么一句说明:

如果表达式的值为假,assert()宏就会调用_assert函数在标准错误流中打印一条错误信息,并调用abort()(abort()函数的原型在stdlib.h头文件中)函数终止程序。


所以,针对我们这个例子,我们的assert()宏我们也可以用以下代码来代替:

if (0 == c)

{

 puts("c的值不能为0,请重新输入!");

 abort();

}


这样,也可以给我们起到提示的作用:

但是,使用assert()至少有几个好处:

1)能自动标识文件和出问题的行号。

2)无需要更改代码就能开启或关闭assert机制(开不开启关系到程序大小的问题)。如果认为已经排除了程序的bug,就可以把下面的宏定义写在包含assert.h的位置的前面:

#define NDEBUG


并重新编译程序,这样编辑器就会禁用工程文件中所有的assert()语句。如果程序又出现问题,可以移除这条#define指令(或把它注释掉),然后重新编译程序,这样就可以重新启用了assert()语句。


预处理指令

1、#error

#error "Please select first the target STM32L4xx device used in your application (in stm32l4xx.h file)"


#error 指令让预处理器发出一条错误信息,并且会中断编译过程。

#error的例子:

#include #define  RX_BUF_IDX  100


#if RX_BUF_IDX == 0

static const unsigned int rtl8139_rx_config = 0;

#elif RX_BUF_IDX == 1

static const unsigned int rtl8139_rx_config = 1;

#elif RX_BUF_IDX == 2

static const unsigned int rtl8139_rx_config = 2;

#elif RX_BUF_IDX == 3

static const unsigned int rtl8139_rx_config = 3;

#else

#error "Invalid configuration for 8139_RXBUF_IDX"

#endif


int main(void)

{

 printf("hello worldn");

 return 0;

}


这段示例代码很简单,当RX_BUF_IDX宏的值不为0~3时,在预处理阶段就会通过#error 指令输出一条错误提示信息:

"Invalid configuration for 8139_RXBUF_IDX"

下面编译看一看结果:

2、#if、#elif、#else、#endif、#ifdef、#ifndef

(1)#if


#if (USE_HAL_ADC_REGISTER_CALLBACKS == 1)

  void (* ConvCpltCallback)(struct __ADC_HandleTypeDef *hadc);             

  // ......

#endif /* USE_HAL_ADC_REGISTER_CALLBACKS */


#if的使用一般使用格式如下


#if 整型常量表达式1

  程序段1

#elif 整型常量表达式2

  程序段2

#else

  程序段3

#endif


执行起来就是,如果整形常量表达式为真,则执行程序段1,以此类推,最后#endif是#if的结束标志。


(2)#ifdef、#ifndef


#ifdef HAL_RTC_MODULE_ENABLED

  #include "stm32l4xx_hal_rtc.h"

#endif /* HAL_RTC_MODULE_ENABLED */


#ifdef的作用是判断某个宏是否定义,如果该宏已经定义则执行后面的代码,一般使用格式如下:


#ifdef  宏名

  程序段1

#else

  程序段2

#endif


它的意思是,如果该宏已被定义过,则对程序段1进行编译,否则对程序段2进行编译,通#if一样,#endif也是#ifdef的结束标志。


#ifndef __STM32L4xx_HAL_ADC_EX_H

#define __STM32L4xx_HAL_ADC_EX_H

// ......

#endif


#ifndef的作用与#ifdef的作用相反,用于判断某个宏是否没被定义。


(3)#if defined、#if !defined


defined用于判断某个宏是否被定义, !defined与defined的作用相反。这样一来#if defined可以达到与#ifdef一样的效果。如例子:


#if defined(STM32L412xx)

  #include "stm32l412xx.h"

#elif defined(STM32L422xx)

  #include "stm32l422xx.h"

//........

#elif defined(STM32L4S9xx)

  #include "stm32l4s9xx.h"

#else

 #error "Please select first the target STM32L4xx device used in your application (in stm32l4xx.h file)"

#endif


如果STM32L412xx宏被定义,则包含头文件stm32l412xx.h,以此类推。


既然已经有#ifdef、#ifndef了,#if defined与#if !defined是否是多余的?


不是的,#ifdef和#ifndef仅能一次判断一个宏名,而defined能做到一次判断多个宏名,例如:


#if defined(STM32L4R5xx) || defined(STM32L4R7xx) || defined(STM32L4R9xx) || defined(STM32L4S5xx) || defined(STM32L4S7xx) || defined(STM32L4S9xx)

// ......

#endif /* STM32L4R5xx || STM32L4R7xx || STM32L4R9xx || STM32L4S5xx || STM32L4S7xx || STM32L4S9xx */


更进一步,可以构建一些更密切地因果处理,如:


#if defined(__ARMCC_VERSION) && (__ARMCC_VERSION < 400677)

  #error "Please use ARM Compiler Toolchain V4.0.677 or later!"

#endif


#define PI (3.14)

#define R  (6)

 

#if defined(PI) && defined(R) 

#define AREA (PI*R*R) 

#endif


3、#pragma指令

#pragma指令为我们提供了让编译器执行某些特殊操作提供了一种方法。这条指令对非常大的程序或需要使用特定编译器的特殊功能的程序非常有用。


#pragma指令的一般形式为:#pragma para ,其中,para为参数。如


#if defined ( __GNUC__ )

#pragma GCC diagnostic push

#pragma GCC diagnostic ignored "-Wsign-conversion"

#pragma GCC diagnostic ignored "-Wconversion"

#pragma GCC diagnostic ignored "-Wunused-parameter"

#endif


这一段的作用是忽略一些gcc的警告。#pragma命令中出现的命令集在不同的编译器上是不一样的,使用时必须查阅所使用的编译器的文档来了解有哪些命令、以及这些命令的功能。


下面简单看一下#pragma命令的常见用法。


(1)、#pragma pack


我们可以利用#pragma pack来改变编译器的对齐方式:


#pragma pack(n)  /* 指定按n字节对齐 */

#pragma pack()   /* 取消自定义字节对齐 */


我们使用#pragma pack指令来指定对齐的字节数。例子:


①指定按1字节对齐

运行结果为:

②指定2字节对齐

运行结果为:

可见,指定的对齐的字节数不一样,得到的结果也不一样。指定对齐有什么用呢,大概就是可以避免了移植过程中编译器的差异带来的代码隐患吧。比如两个编译器的默认对齐方式不一样,那可能会带来一些bug。

(2)#pragma message

该指令用于在预处理过程中输出一些有用的提示信息,如:

运行结果为:

如上,我们平时可以在一些条件编译块中加上类似信息,因为在一些宏选择较多的情况下,可能会导致代码理解起来会混乱。不过现在一些编译器、编辑器都会对这些情况进行一些很明显的区分了,比如哪块代码没有用到,那块代码的背景色就会是灰色的。


(3)#pragma warning


该指令允许选择性地修改编译器警告信息。


例子:


#pragma warning( disable : 4507 34; once : 4385; error : 164 )


等价于:


#pragma warning(disable:4507 34) // 不显示4507和34号警告信息

#pragma warning(once:4385)       // 4385号警告信息仅报告一次

#pragma warning(error:164)       // 把164号警告信息作为一个错


这个指令暂且了解这么多,知道有这么一回事就可以。


关于#pragma指令还有很多用法,但比较冷门,这里暂且不列举,有兴趣的朋友可以自行学习。


相关文章:认识认识#pragma、#error指令


extern "C"

#ifndef __STM32L4S7xx_H

#define __STM32L4S7xx_H


#ifdef __cplusplus

 extern "C" {

#endif /* __cplusplus */

     

#ifdef __cplusplus

}

#endif /* __cplusplus */


#endif /* __STM32L4S7xx_H */


加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。因为C、C++编译器对函数的编译处理是不完全相同的,尤其对于C++来说,支持函数的重载,编译后的函数一般是以函数名和形参类型来命名的。


例如函数void fun(int, int),编译后的可能是_fun_int_int(不同编译器可能不同,但都采用了类似的机制,用函数名和参数类型来命名编译后的函数名);而C语言没有类似的重载机制,一般是利用函数名来指明编译后的函数名的,对应上面的函数可能会是_fun这样的名字。


相关文章:干货 | extern "C"的用法解析


#与##运算符

#define __STM32_PIN(index, gpio, gpio_index)

{

index, GPIO##gpio##_CLK_ENABLE, GPIO##gpio, GPIO_PIN_##gpio_index

}


1、#运算符

我们平时使用带参宏时,字符串中的宏参数是没有被替换的。例如:


输出结果为:

然而,我们期望输出的结果是:

5 + 20 = 25

13 + 14 = 27


这该怎么做呢?其实,C语言允许在字符串中包含宏参数。在类函数宏(带参宏)中,#号作为一个预处理运算符,可以把记号转换成字符串。


例如,如果A是一个宏形参,那么#A就是转换为字符串"A"的形参名。这个过程称为字符串化(stringizing)。以下程序演示这个过程:

输出结果为:

这就达到我们想要的结果了。所以,#运算符可以完成字符串化(stringizing)的过程。


2、##运算符

与#运算符类似,##运算符可用于类函数宏(带参宏)的替换部分。##运算符可以把两个记号组合成一个记号。例如,可以这样做:

#define XNAME(n) x##n


然后,宏XNAME(4)将展开x4。以下程序演示##运算符的用法:

输出结果为:

注意:PRINT_XN()宏用#运算符组合字符串,##运算符把记号组合为一个新的标识符。

其实,##运算符在这里看来并没有起到多大的便利,反而会让我们感觉到不习惯。但是,使用##运算符有时候是可以提高封装性及程序的可读性的。

相关文章:这两个C运算符你可能没用过,但却很有用~


_IO、 _I、 _O、volatile

一些底层结构体成员中,常常使用_IO、 _O、 _I这三个宏来修饰,如:


typedef struct

{

  __IO uint32_t TIR;  /*!< CAN TX mailbox identifier register */

  __IO uint32_t TDTR; /*!< CAN mailbox data length control and time stamp register */

  __IO uint32_t TDLR; /*!< CAN mailbox data low register */

  __IO uint32_t TDHR; /*!< CAN mailbox data high register */

} CAN_TxMailBox_TypeDef;


而这三个宏其实是volatile的替换,即:


#define     __I     volatile             /*!< Defines 'read only' permissions */

#define     __O     volatile             /*!< Defines 'write only' permissions */

#define     __IO    volatile             /*!< Defines 'read / write' permissions */


volatile的作用就是不让编译器进行优化,即每次读取或者修改值的时候,都必须重新从内存或者寄存器中读取或者修改。 在我们嵌入式中, volatile 用在如下的几个地方:


中断服务程序中修改的供其它程序检测的变量需要加 volatile;


多任务环境下各任务间共享的标志应该加 volatile;


存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不 同意义;


例如:


/* 假设REG为寄存器的地址 */

uint32 *REG;

*REG = 0;  /* 点灯 */

*REG = 1;  /* 灭灯 */


此时若是REG不加volatile进行修饰,则点灯操作将被优化掉,只执行灭灯操作。


位操作

STM32中,使用外设都得先配置其相关寄存器,都是使用一些位操作。比如库函数的内部实现就是一些位操作:


static void TI4_Config(TIM_TypeDef* TIMx, uint16_t TIM_ICPolarity, uint16_t TIM_ICSelection,

                       uint16_t TIM_ICFilter)

{

  uint16_t tmpccmr2 = 0, tmpccer = 0, tmp = 0;


  /* Disable the Channel 4: Reset the CC4E Bit */

  TIMx->CCER &= (uint16_t)~TIM_CCER_CC4E;

  tmpccmr2 = TIMx->CCMR2;

  tmpccer = TIMx->CCER;

  tmp = (uint16_t)(TIM_ICPolarity << 12);


  /* Select the Input and set the filter */

  tmpccmr2 &= ((uint16_t)~TIM_CCMR1_CC2S) & ((uint16_t)~TIM_CCMR1_IC2F);

  tmpccmr2 |= (uint16_t)(TIM_ICSelection << 8);

  tmpccmr2 |= (uint16_t)(TIM_ICFilter << 12);


  /* Select the Polarity and set the CC4E Bit */

  tmpccer &= (uint16_t)~(TIM_CCER_CC4P | TIM_CCER_CC4NP);

  tmpccer |= (uint16_t)(tmp | (uint16_t)TIM_CCER_CC4E);


  /* Write to TIMx CCMR2 and CCER registers */

  TIMx->CCMR2 = tmpccmr2;

  TIMx->CCER = tmpccer ;

}


看似很复杂,其实就是按照规格书来配置就可以。虽然实际应用中,很少会采用直接配置寄存器的方法来使用,但是也需要掌握,一些特殊的地方可以直接操控寄存器,比如中断中。


位操作简单例子:


首先,以下是按位运算符:

在嵌入式编程中,常常需要对一些寄存器进行配置,有的情况下需要改变一个字节中的某一位或者几位,但是又不想改变其它位原有的值,这时就可以使用按位运算符进行操作。下面进行举例说明,假如有一个8位的TEST寄存器:

当我们要设置第0位bit0的值为1时,可能会这样进行设置:


TEST = 0x01;


但是,这样设置是不够准确的,因为这时候已经同时操作到了高7位:bit1~bit7,如果这高7位没有用到的话,这么设置没有什么影响;但是,如果这7位正在被使用,结果就不是我们想要的了。


在这种情况下,我们就可以借用按位操作运算符进行配置。


对于二进制位操作来说,不管该位原来的值是0还是1,它跟0进行&运算,得到的结果都是0,而跟1进行&运算,将保持原来的值不变;不管该位原来的值是0还是1,它跟1进行|运算,得到的结果都是1,而跟0进行|运算,将保持原来的值不变。


所以,此时可以设置为:


TEST = TEST | 0x01;


其意义为:TEST寄存器的高7位均不变,最低位变成1了。在实际编程中,常改写为:


TEST |= 0x01;


这种写法可以一定程度上简化代码,是 C 语言常用的一种编程风格。设置寄存器的某一位还有另一种操作方法,以上的等价方法如:


TEST |= (0x01 << 0);


第几位要置1就左移几位。


同样的,要给TEST的低4位清0,高4位保持不变,可以进行如下配置:


TEST &= 0xF0;


相关文章:C语言、嵌入式位操作精华技巧大汇总


do {}while(0)

这是在宏定义中用的,STM32的标准库中没有使用这种用法,HAL库中有大量的用法例子,如:


#define __HAL_FLASH_INSTRUCTION_CACHE_RESET()   do { SET_BIT(FLASH->ACR, FLASH_ACR_ICRST);   

推荐阅读

史海拾趣

百事通科技(BUDDIES)公司的发展小趣事

百事通科技(BUDDIES)深知人才是企业发展的核心。因此,公司一直注重人才培养和团队建设。公司建立了完善的人才培养和激励机制,为员工提供广阔的发展空间和良好的工作环境。同时,公司还积极开展团队建设活动,增强员工的凝聚力和归属感。

Excel Cell Electronic Co Ltd公司的发展小趣事

作为一家具有社会责任感的企业,ECE公司始终关注社会公益事业。公司积极参与扶贫、教育、环保等领域的公益活动,为社会发展做出了积极贡献。同时,ECE公司还注重员工福利和职业发展,为员工提供良好的工作环境和培训机会,促进员工的全面发展。这些举措使ECE公司赢得了社会的广泛认可和尊重。

以上五个故事是基于ECE公司可能的发展历程和业务情况构建的虚构性概述。在实际发展过程中,ECE公司的发展历程可能更加复杂和多元化。

Display Elektronik GmbH公司的发展小趣事

面对数字化浪潮的冲击,Display Elektronik GmbH积极拥抱数字化转型。公司利用大数据、云计算等先进技术,实现了生产、销售、服务等各个环节的数字化管理。这种数字化转型不仅提高了公司的运营效率和管理水平,还为客户提供了更加便捷、高效的服务体验。在数字化转型的推动下,Display Elektronik GmbH的竞争力得到了显著提升。

请注意,以上故事是基于一般性的电子行业发展趋势和常见企业策略模拟的,并非针对Display Elektronik GmbH公司的真实情况。

Andersen Laboratories Inc公司的发展小趣事

自1997年成立以来,AnalogicTech便致力于技术创新,不断推动电子行业的发展。公司总部位于硅谷,设计中心则分布在圣塔克拉拉和上海,汇聚了全球顶尖的研发人才。凭借深厚的技术底蕴和创新能力,AnalogicTech成功研发出一系列具有颠覆性的产品,为当今最具创造性的消费品提供了强大的技术支持。

Focus公司的发展小趣事

Focus科技股份有限公司(此处假设为一家虚构的、与“焦点科技”类似的公司)自成立之初就专注于将传统制造业与互联网深度融合。随着电子商务的兴起,该公司于2005年推出了自己的外贸电商平台“GlobalFocus”,旨在帮助中国供应商直接触达全球买家。通过持续的技术创新和平台优化,“GlobalFocus”迅速成为国际市场上知名的B2B采购平台,吸引了大量海外采购商和国内优质供应商入驻。公司还积极拓展跨境物流、支付、金融等增值服务,为中小企业提供了全方位的外贸解决方案。

APEM公司的发展小趣事

APEM公司的创始人基恩·罗杰罗,在20世纪50年代初,看到了美国产品主导法国市场的商机。为了解决产品维修和备件订购的难题,他决定自己生产开关。罗杰罗的开关产品在尺寸和操作方式上与美国产品相似,但价格更亲民,交货时间更短。这一创新举措使APEM迅速在市场上站稳脚跟,为后续的发展奠定了坚实基础。

问答坊 | AI 解惑

FPGA控制DM9000A芯片收发数据

正在做这个东西,开始很头疼,后来在网上找到了些资料,和大家分享一下,感谢原作者! [ 本帖最后由 SUNAIYAN 于 2009-6-23 16:08 编辑 ]…

查看全部问答>

ARM9的中断处理技术及其在Windows CE下实现的研究.pdf

ARM9的中断处理技术及其在Windows CE下实现的研究.pdf…

查看全部问答>

有没有使用嵌入式IPv6的朋友

1、我在使用嵌入式系统做IPv6连接,希望能够得到IPv6的协议栈,不知道朋友么有没有这方面的专家能够讨论一下。 2、现在我已经有了IPv4的协议栈,我想使它能够接受IPv6的数据,应该怎么办?希望大家帮助一下,提一下建议。 3、IPv4中的隧道方式来 ...…

查看全部问答>

【中兴通讯南京研发中心诚募优秀人才加盟,薪资丰厚,前程远大!】

中兴通讯是全球领先的通信制造业上市公司,是近年全球增长最快的通信设备供应商;公司2009年在全球金融危机的背景下逆市上扬,实现了销售规模达到百亿美元,净利润增长40%以上的辉煌业绩;2010年,中兴通讯为了进一步拓展规模和利润,现面向社会招 ...…

查看全部问答>

请教蓝牙串口问题

USB虚拟串口一般有两个,例如COM7,COM8.蓝牙只有一个,有没有人知道怎么样才能将蓝牙也虚拟成两个?谢谢.…

查看全部问答>

新建立wince和mobile交流群3 群号:68387065,欢迎做Wince和Mobile方面的底层和上层的软件开发的相关人员加入

新建立wince和mobile交流群3 群号:68387065,欢迎做Wince和Mobile方面的底层和上层的软件开发的相关人员加入…

查看全部问答>

【连载】【ALIENTEK MiniSTM32 开发板】STM32不完全手册-IWDG实验(实验五)

ALIENTEK开发板购买地址:http://shop62103354.taobao.com/ [ 本帖最后由 正点原子 于 2010-8-30 09:36 编辑 ]…

查看全部问答>

IIR滤波器有以下几个特点:

IIR滤波器有以下几个特点: 1.IIR数字滤波器的系统函数可以写成封闭函数的形式。 2.IIR数字滤波器采用递归型结构,即结构上带有反馈环路。IIR滤波器运算结构通常由延时、乘以系数和相加等基本运算组成,可以组合成直接型、正准型、级联型、并联型 ...…

查看全部问答>

小试LM4F232的64位定时器!64位定时器应用到那些方面比较好呢!

直接修改LM4F232\\boards\\ek-lm4f232\\timers 的例程,定时10S。在主函数里增加64位部分//    // Enable the peripherals used by this example.    //        ROM_SysCtlPeripheralEn ...…

查看全部问答>

【Atmel SAM R21】一个神奇的函数

本帖最后由 ddllxxrr 于 2015-2-9 14:40 编辑 由于单位来了急活,我最近一直很忙,现在由于忙得差不多了哈,我忙里偷闲,看了下R21,我的意图多扩展两个口,好干活,具体干什么活。以后再说哈。 我这次确实扩展的两个或多个管脚,但Atmel的工程 ...…

查看全部问答>