一、C语言弱符号和弱引用

__attribute__ 是一个编译器指令,其实是 GNU C 的一种机制,本质是一个编译器的指令,在声明的时候可以提供一些属性,在编译阶段起作用,来做多样化的错误检查和高级优化。

用于在 C,C++,Objective-C 中修饰变量、函数、参数、方法、类等。

合理使用 __attribute__ 有什么好处?
  • 给编译器提供上下文,帮助编译器做优化,合理使用可以收到显著的优化效果。
  • 编译器会根据 __attribute__ 产生一些编译警告,使代码更规范。
  • 给代码阅读者提供必要的注解,助其理解代码意图。

总之,__attribute__ 起到了给编译器提供上下文的作用,如果错误的使用 __attribute__ 指令,因为给编译器提供了错误的上下文,由此引起的错误通常很难被发现。

强符号和弱符号

在同一作用域下不能定义同一个变量或函数,很多C语言学习者都理所当然地这么认为。

这个其实是是有所偏颇的,GNU C对标准C语言进行了扩展,在GCC中,对于符号(在编译时,变量和函数都被抽象成符号)而言,存在着强符号和弱符号之分。

是的,是否支持这个特性是由不同的C语言标准决定的。

对于C/C++而言,编译器默认函数和已初始化的全局变量为强符号,而未初始化的全局变量为弱符号。

在编程者没有显示指定时,编译器对强弱符号的定义会有一些默认行为,同时开发者也可以对符号进行指定,使用"attribute((weak))"来声明一个符号为弱符号。

定义一个相同的变量,当两者不全是强符号时,gcc在编译时并不会报错,而是遵循一定的规则进行取舍:

  • 当两者都为强符号时,重复定义的报错:redefinition of 'xxx'
  • 当两者为一强一弱时,选取强符号的值
  • 当两者同时为弱时,选择其中占用空间较大的符号,这个其实很好理解,编译器不知道编程者的用意,选择占用空间大的符号至少不会造成诸如溢出、越界等严重后果。

在默认的符号类型情况下,强符号和弱符号是可以共存的,类似于这样:

int x;
int x = 1;
  • 1.
  • 2.

编译不会报错,在编译时x的取值将会是1。

注意,这里可以使用__attribute__((weak))将强符号转换为弱符号,却不能与一个强符号共存,类似于这样:

int __attribute__((weak)) x = 0;
int x = 1;
  • 1.
  • 2.

编译器将报重复定义错误。

强引用和弱引用

除了强符号和弱符号的区别之外,GNUC还有一个特性就是强引用和弱引用。

我们知道的是,编译器在编译阶段只负责将源文件编译成目标文件(即二进制文件),然后由链接器对所有二进制文件进行链接操作。

编译器默认所有的变量和函数为强引用,同时编程者可以使用__attribute__((weakref))来声明一个函数。

注意这里是声明而不是定义,既然是引用,那么就是使用其他模块中定义的实体,对于函数而言,我们可以使用这样的写法:

__attribute__((weakref)) void func(void);
  • 1.

然后在函数中调用func(),如果func()没有被定义,则func的值为0,如果func被定义,则调用相应func,在《程序员的自我修养》这本书中有介绍,它是这样写的:

__attribute__((weakref)) void func(void);
void main(void)
{
    if(func) 
    {
        func();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

但是在现代的编译系统中,这种写法却是错误的,编译虽然通过(有警告信息),但是却不正确:

warning: ‘weakref’ attribute should be accompanied with an ‘alias’ attribute [-Wattributes]
  • 1.

警告显示:weakref需要伴随着一个别名才能正常使用。

强/弱符号和强/弱引用的作用

这种弱符号、弱引用的扩展机制在库的实现中非常有用。

我们在库中可以使用弱符号和弱引用机制,这样对于一个弱符号函数而言,用户可以自定义扩展功能的函数来覆盖这个弱符号函数。

同时我们可以将某些扩展功能函数定义为弱引用,当用户需要使用扩展功能时,就对其进行定义,链接到程序当中。

如果用户不进行定义,则链接也不会报错,这使得库的功能可以很方便地进行裁剪和组合。

注意:C标准里根本没有提到强、弱符号。这只是GCC这个实现定义的特性,在MS C编译器里是不存在这个概念的。

二、MOS管驱动电路设计

  一般认为MOSFET是电压驱动的,不需要驱动电流。然而,在MOS的G S两级之间有结电容存在,这个电容会让驱动MOS变的不那么简单。

嵌入式分享合集129_编译器

如果不考虑纹波和EMI等要求的话,MOS管开关速度越快越好,因为开关时间越短,开关损耗越小,而在开关电源中开关损耗占总损耗的很大一部分,因此MOS管驱动电路的好坏直接决定了电源的效率。

    对于一个MOS管,如果把GS之间的电压从0拉到管子的开启电压所用的时间越短,那么MOS管开启的速度就会越快。与此类似,如果把MOS管的GS电压从开启电压降到0V的时间越短,那么MOS管关断的速度也就越快。

    由此我们可以知道,如果想在更短的时间内把GS电压拉高或者拉低,就要给MOS管栅极更大的瞬间驱动电流。

    大家常用的PWM芯片输出直接驱动MOS或者用三极管放大后再驱动MOS的方法,其实在瞬间驱动电流这块是有很大缺陷的。

    比较好的方法是使用专用的MOSFET驱动芯片如TC4420来驱动MOS管,这类的芯片一般有很大的瞬间输出电流,而且还兼容TTL电平输入,MOSFET驱动芯片的内部结构如下:

嵌入式分享合集129_三极管_02

 

 MOS驱动电路设计需要注意的地方:

    因为驱动线路走线会有寄生电感,而寄生电感和MOS管的结电容会组成一个LC振荡电路,如果直接把驱动芯片的输出端接到MOS管栅极的话,在PWM波的上升下降沿会产生很大的震荡,导致MOS管急剧发热甚至爆炸,一般的解决方法是在栅极串联10欧左右的电阻,降低LC振荡电路的Q值,使震荡迅速衰减掉。

    因为MOS管栅极高输入阻抗的特性,一点点静电或者干扰都可能导致MOS管误导通,所以建议在MOS管G S之间并联一个10K的电阻以降低输入阻抗。

    如果担心附近功率线路上的干扰耦合过来产生瞬间高压击穿MOS管的话,可以在GS之间再并联一个18V左右的TVS瞬态抑制二极管。

    TVS可以认为是一个反应速度很快的稳压管,其瞬间可以承受的功率高达几百至上千瓦,可以用来吸收瞬间的干扰脉冲。

    MOS管驱动电路参考:

嵌入式分享合集129_编译器_03

MOS管驱动电路的布线设计

    MOS管驱动线路的环路面积要尽可能小,否则可能会引入外来的电磁干扰。

    驱动芯片的旁路电容要尽量靠近驱动芯片的VCC和GND引脚,否则走线的电感会很大程度上影响芯片的瞬间输出电流。

 

嵌入式分享合集129_编译器_04

    常见的MOS管驱动波形,如下图。

嵌入式分享合集129_编译器_05

 

 如果出现了这样圆不溜秋的波形就等着核爆吧。有很大一部分时间管子都工作在线性区,损耗极其巨大。

    一般这种情况是布线太长电感太大,栅极电阻都救不了你,只能重新画板子。

嵌入式分享合集129_编译器_06

   高频振铃严重的毁容方波。

    在上升下降沿震荡严重,这种情况管子一般瞬间死掉,跟上一个情况差不多,进线性区。

    原因也类似,主要是布线的问题。又胖又圆的肥猪波。

    上升下降沿极其缓慢,这是因为阻抗不匹配导致的。

    芯片驱动能力太差或者栅极电阻太大。

    果断换大电流的驱动芯片,栅极电阻往小调调就OK了。

    打肿脸充正弦的生于方波他们家的三角波。

    驱动电路阻抗超大发了。此乃管子必杀波。解决方法同上。

嵌入式分享合集129_三极管_07

  大众脸型,人见人爱的方波。

    高低电平分明,电平这时候可以叫电平了,因为它平。边沿陡峭,开关速度快,损耗很小,略有震荡,可以接受,管子进不了线性区,强迫症的话可以适当调大栅极电阻。

嵌入式分享合集129_嵌入式硬件_08

 

方方正正的帅哥波,无振铃无尖峰无线性损耗的三无产品,这就是最完美的波形了。

 

三、单片机接口电路设计中的电流倒灌和电平转换

  接口电路的设计在电单片机应用场合中还是很重要的,因为如果接口电路没有设计好,严重就会烧芯片,或者烧芯片IO口,轻者就会导致工作紊乱,工作不正常。

    有时候这种问题自己在设计调试的时候根本发现不了,在批量生产或者用户在使用的时候才出现芯片被烧掉,或者IO口被烧掉。如果我们在设计的时候能考虑到接口的一些问题就可以减少,提高产品的可靠性。

    下面我们就从电流倒灌问题和电平匹配问题进行叙述。

电流倒灌

1 概念

    倒灌就是电流流进IC内部,电流总是流入电势低的地方。比如说电压源,一般都是输出电流,但是如果有另一个电源同时存在,并且电势高于这个电源,电流就会流入这个电源,称为倒灌。

2 危害

    1. 电流太大会将使IO口上的钳位二极管迅速过载并使其损坏。

    2. 会使单片机复位不成功。

    3. 会使可编程器件程序紊乱。

    4.会出现闩锁效应。

3 原因

嵌入式分享合集129_单片机_09

 如上图,STM32的IO口框图。

    当两个单片机进行串口通信,如果其中一个单片机断电,另一个单片机继续供电,正常运行。那么没有断电的单片机的IO口给断电的单片机的IO口供电,并同通过上拉保护二极管向断电的单片机进行供电。或者说两个单片机供电电压不一样,电流就会从供电高的一方流向供电低的一方。

4 解决办法

嵌入式分享合集129_三极管_10

如上图,加一个小电阻,可以防止过流损坏二极管D1。还可以进行阻抗匹配,因为信号源的阻抗很低,跟信号线之间阻抗不匹配,串上一个电阻后,可改善匹配情况,以减少反射,避免振荡等。也可以减少信号边沿的陡峭程度,从而减少高频噪声以及过冲等。但不能解决灌流在Vcc上建立电压。一般情况下就会选择串电阻,取值范围是几欧到1K欧,根据实际情况而定,小编我喜欢取330欧。

嵌入式分享合集129_编译器_11

 

 如上图,在信号线上加二极管D3及上拉电阻R1,D3用于阻断灌流通路,R1解决前级输出高电平时使G1的输入保持高电平。

    此方法既可解决灌流损坏二极管D1的问题,又可解决灌流在Vcc上建立电压。缺点只适用于速率不快的电路上。如果单片机IO口比较脆弱,或者两边电压不也一样需要低成本进行电平转换,且是但一方向,速率比较低(比如串口)的时候就可以选择该方案。二极管要选择肖特基二极管才比较好

电平转换

    在电路设计过程中,会碰到处理器MCU的I/O电平与模块的I/O电平不相同的问题,为了保证两者的正常通信,需要进行电平转换。如果两边的电平不一样就直接连接进行通信,像TTL电平就会出现上一节将的那样电流倒灌现象。

    设计电平转换电路需要几个问题:

    (1)VOH>VIH;VOL<vil

嵌入式分享合集129_单片机_12

  各种电平的电压范围,如上图。

    (2)对于多电源系统,某些器件不允许输入电平超过电源电压,针对有类似要求的器件,电路上应适当做些保护。

    (3)电平转换电路会影响通信速度,所以使用时应当注意通信速率上的要求。

1 NPN三极管电平转换

嵌入式分享合集129_嵌入式硬件_13

    这个电平转换就是两级三极管电路组成。三极管只能单向进行转换,而且元器件比较多。

2 NMOS电平转换

嵌入式分享合集129_三极管_14

该电路可实现双向传输,使用条件是VCC2>VCC1+0.7V,这个电路也是小编我常用的电路。

    其工作过程是:

    Port1向Port2传输:

    (1)Port1高电平时,NMOS的Ugs=0V截止,Port2端的电压为VCC2高电平。

    (2)Port1低电平时,NMOS的Ugs=3.3V导通,Port2端的电压为Port1端的电压低电平。

    Port2向Port1传输:

    (1)Port2高电平时,NMOS的Ugs=0V截止,Port1端的电压为VCC1--高电平。

    (2)Port2低电平时,NMOS的体二极管导通,使得Vs的电压为0.7V左右,那么Ugs=VCC1-0.7V,只要选择的开启电压小于Ugs电压就可以让MOS管导通,Port1端的电压为Port2端的电压--低电平。

3 使用专用电平芯片转换电平

    使用专用的电平转换芯片,分别给输入和输出信号提供不同的电压,转换由芯片内部完成,例如PCA9306DCTR等电平转换芯片。专用芯片是最可靠的电平转换方案。

 

嵌入式分享合集129_嵌入式硬件_15

优势:

    1) 驱动能力强:专用芯片的输出一般都使用了CMOS工艺,输出驱动10mA不在话下。

    2) 漏电流几乎为0:内部是一些列的放大、比较器,输入阻抗非常高,一般都达到数百K。漏电流基本都是nA级别的。

    3) 路数较多:专用芯片针对不同的应用,从2路到数十路都有,十分适合对面积要求高的场合。

    4) 速率高:专用芯片由于集成度较高,工艺较高,,速率从数百K到数百M的频率都可以做。

    劣势:

    1) 成本:专用芯片集众多优势于一身,就是成本是最大的劣势,一个普通的数百K速率的4通道电平转换芯片,价格至少要1元人民币以上,如果使用三极管做,成本2毛钱都不到。

嵌入式分享合集129_嵌入式硬件_16


4 使用电阻分压转换电平 

优势:

    1) 便宜:便宜是最大的优点,2个电阻一分钱不到;

    2) 容易实现:电阻采购容易,占用面积小。

    劣势:

    1) 速度:分压法为了降低功耗,使用K级别以上的电阻,加上电路和器件的分布和寄生电容,速率很难上去,一般只能应用于100K以内的频率。

    2) 驱动能力:由于使用了大阻值的电阻,驱动能力被严格控制,并不适合需要高驱动能力的场合,例如LED灯等

    3) 漏电:漏电是该方案最大的缺点,由于通过电阻直连,左右两端的电压会流动,从而互相影响。例如,RS232接口采用该方案,上电瞬间外设就给主芯片提供2.8V的电平,轻则影响时序导致主芯片无法启动,重则导致主芯片闩锁效应,烧毁芯片。

5 使用电阻限流转换电平

嵌入式分享合集129_三极管_17

 

 优势:

    1) 便宜:便宜是最大的优点,只需要一个电阻就解决。

    2) 容易实现。

    劣势:

    1) 电阻选值不是很容易选择,需要对芯片内部很熟悉。

6 使用二极管转换电平

嵌入式分享合集129_嵌入式硬件_18

优势:

    1) 漏电流小:由于二极管的漏电流非常小(uA级),可以单向防止电源倒灌,防止电流倒灌。

    2) 容易实现。

    劣势:

    1) 电平误差大:主要是二极管的正向压降较大,容易超出芯片的工作电压范围。

    2) 单向防倒灌:只能单向防止倒灌,不能双向防止倒灌。

    3) 速度和驱动能力不理想:由于电阻限流,驱动速度和能力均不理想,只能应用在100K以内的频率。

四、用三极管来配合单片机IO口驱动负载

驱动继电器的时候,通常我们会采用三极管来配合单片机IO口。至于为什么不直接用单片机IO口驱动,非得加个三极管,在上一篇推文中我们已经做过计算了。至于为什么采用三极管,更大的原因是因为三极管属于流控型器件,也就是说三极管的这个电子开关的闭合与断开是通过电流开控制的,并且所需要的电流非常小。三极管基极驱动电压只要高于Ube(一般是0.7V)就能导通。

嵌入式分享合集129_三极管_19

现在的大家都讲究低功耗,供电电压也越来越低,一般单片机供电为3.3V,所以它的I/O最高电压也就是3.3V。

3.3V电压肯定是大于Ube的,所以直接在基极串联一个合适的电阻,让三极管工作在饱和区就可以了。Ib=(VO-0.7V)/R2。根据公式计算,上图中Ib的电流应该等于(5-0.7)/(4.7x1000),大于是0.918mA,实际仿真测试结果为0.628mA,基本符合实际值,三极管能正常开启和闭合实现控制,可以正常的实现控制负载(此处为LED灯)。

到这可能会有硬件基础好的小伙伴要说了,MOS管也可以啊,为什么非得用三极管呢?

其原因在于,MOS管是电压控制型,驱动电压必须高于阈值电压Vgs(TH)才能正常导通,不同MOS管的阈值电压是不一样的,一般为3-5V左右,饱和驱动电压可在6-8V。

前面说过现在单片机的供电基本都是3.3V,IO口最高电压也是3.3V,大部分的MOS管的饱和电压>3.3V,如果用3.3V来驱动的话,很可能MOS管根本就打不开,或者处于半导通状态。在半导通状态下,管子的内阻很大,驱动小电流负载可以这么用。但是大电流负载就不行了,内阻大,管子的功耗大,MOS管很容易就烧坏了。所以,一般选择三极管来配合单片机IO口驱动。

当然,MOS管得驱动电流很大,在更多的需要大功率的驱动电路中,通过会采用但机关配合MOS一起来实现大电流的驱动运用场景,比如下面这个电路图就是。

嵌入式分享合集129_单片机_20

 I/O口驱动三极管后再驱动MOS管

当I/O为高电平时,三极管导通,MOS管栅极被拉低,负载RL不工作。

当I/O为低电平时,三极管不导通,MOS管通过电阻R3,R4分压,为栅极提供合适的阈值电压,MOS管导通,负载RL正常工作。

结合以上的分析,相比大家应该都清除了,通常情况下大家习惯用三极管来连接单片机IO口实现驱动,是因为三极管是流控型器件,但是三极管的驱动能力比较弱。在需要大功率驱动的地方,通常会采用三极管再去控制MOS管实现最终的控制。

直接用MOS管来连接单片机的IO实现驱动也是可以的,但这样的MOS管型号不好找。小编在立创商城上所搜了一下,也有这样的器件,控制电压最低可以到1V,驱动电流峰值2.3A,持续1.6A;相同封装的三极管8050,驱动的Ic电流只能到600mA。

可见MOS管的驱动能力是三极管3-4倍,所以对负载电流有要求的都使用MOS管。大的驱动能力,带来的会是成本的增加,搜索结果中MOS管的价格几乎是三极管的10倍。

嵌入式分享合集129_编译器_21

 

所以,在要求不高,成本低的应用场合,一般使用三极管作为开关管。 

五、STM32CubeMX-实时时钟(RTC)

RTC简介

    实时时钟 (RTC) 是一个独立的 BCD 定时器/计数器。RTC 提供具有可编程闹钟中断功能的日历时钟 /日历。RTC 还包含具有中断功能的周期性可编程唤醒标志。系统可以自动将月份的天数补偿为 28、29(闰年)、30 和 31 天。只要芯片的备用电源一直供电,RTC上的时间会一直走。

新建工程

    本程序在串口printf工程的基础上修改,复制串口printf的工程,修改文件夹名。点击STM32F746I.ioc打开STM32cubeMX的工程文件重新配置。RTC选择内部唤醒(Internal WakeUp)开启RTC。开启外部低速晶振,PC14,PC15配置。

嵌入式分享合集129_嵌入式硬件_22

    RTC时钟选择为外部低速晶振(LSE),频率为32.768。

    在RTC配置中,设置时间和日期,其他为默认设置。此处设置时间为2016/04/16 16:25:49。

嵌入式分享合集129_编译器_23

 生成报告以及代码,编译程序。

添加应用程序

    在rtc.c文件中可以看到ADC初始化函数。在stm32f7xx_hal_rtc.h头文件中可以看到rtc时间和日期读写操作函数。

    从操作函数中可以看到,时间和日期是以结构体的形式读写的。所以在main.c文件前面申明两个结构体变量存储读取的时间和日期数据。

/* USER CODE BEGIN PV */
/* Private variables --------*/
RTC_DateTypeDef sdatestructure;
RTC_TimeTypeDef stimestructure;
/* USER CODE END PV */
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

    在stm32f7xx_hal_rtc.h头文件中,可以找到RTC_TimeTypeDef,RTC_DateTypeDef这两个结构体的成员变量。

嵌入式分享合集129_嵌入式硬件_24


在while循环中添加应用程序,读取当前的时间和日期,并通过串口发送到电脑上显示。

/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
      /* Get the RTC current Time ,must get time first*/
      HAL_RTC_GetTime(&hrtc, &stimestructure, RTC_FORMAT_BIN);
      /* Get the RTC current Date */
      HAL_RTC_GetDate(&hrtc, &sdatestructure, RTC_FORMAT_BIN);
      /* Display date Format : yy/mm/dd */
      printf("%02d/%02d/%02d\r\n",2000 + sdatestructure.Year, sdatestructure.Month, sdatestructure.Date);
      /* Display time Format : hh:mm:ss */
      printf("%02d:%02d:%02d\r\n",stimestructure.Hours, stimestructure.Minutes, stimestructure.Seconds);
      printf("\r\n");
      HAL_Delay(1000);
}
/* USER CODE END 3 */
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

    程序中使用HAL_RTC_GetTime(),HAL_RTC_GetDate()读取时间和日期,并保存到结构体变量中,然后通过串口输出读取的时间和日期。注意:要先读取时间再读取日期,如果先读取日期在读取时间会导致读取的时间不准确,一直都是原来设置的时间。

实验效果

    编译程序并下载到开发板。打开串口调试助手。设置波特率为115200。串口助手上会显示RTC的时间日期。

 

嵌入式分享合集129_单片机_25