给自己的学习总结帖~~ 这里仅都是c语言 嵌入式相关代码第6季啊

一、单片机掉电检测与数据掉电保存怎么做

单片机在正常工作时,因某种原因造成突然掉电,将会丢失数据存储器(RAM)里的数据。在某些应用场合如测量、控制等领域,单片机正常工作中采集和运算出一些重要数据,待下次上电后需要恢复这些重要数据。

    因此,在一些没有后备供电系统的单片机应用系统中,有必要在系统完全断电之前,把这些采集到的或计算出的重要数据保存在FLASH或EEPROM中。为此,通常做法是在这些系统中加入单片机掉电检测电路与单片机掉电数据保存。

    用法拉电容可简单实现单片机掉电检测与数据掉电保存。电路见下图。这里用6V供电(如7806),为什么用6V不用5V是显而易见的。

c语言-嵌入式专辑6~_字符串

电路中的二极管们一般都起两个作用:

    一是起钳位作用,钳去0.6V,保证使大多数实用5V供电的单片机(比如51单片机)都能在4.5V--5.5V之间的标称工作电压下工作。

而4.5-5.5间这1V电压在0.47F电容的电荷流失时间就是我们将来在单片机掉电检测报警后我们可以规划的预警回旋时间。

    二是利用单向导电性保证向储能电容0.47F/5.5V单向冲电。

两只47欧电阻作用:

    第一,对单片机供电限流。

    一般地单片机电源直接接7805上,这是个不保险的做法,为什么?因为7805可提供高达2A的供电电流,异常时足够把单片机芯片内部烧毁。

    有这个47欧姆电阻保护,即使把芯片或者极性插反也不会烧单片机和三端稳压器,但这个电阻也不能太大,上限不要超过220欧,否则对单片机内部编程时,会编程失败(其实是电源不足)。

    第二,和47UF和0.01UF电容一起用于加强电源滤波。

    第三,对0.47F/5.5V储能电容,串入的这只47欧电阻消除"巨量法拉电容"的上电浪涌.实现冲电电流削峰。

    现在我们算一算要充满0.47F电容到5.5V,即使用5.5A恒流对0.47F电容冲电,也需要0.47秒才能冲到5.5V,因此我们可以知道:

    1.如果没有47欧姆电阻限流,上电瞬间三端稳压器必然因强大过电流而进入自保.

    2.长达0.47秒(如果真有5.5A恒流充电的话)缓慢上电,如此缓慢的上电速率,将使得以微分(RC电路)为复位电路的51单片机因为上电太慢无法实现上电复位.(其实要充满0.47UF电容常常需要几分种).

    3.正因为上电时间太慢,将无法和今天大多数主流的以在线写入(ISP)类单片机与写片上位计算机软件上预留的等待应答时间严重不匹配(一般都不大于500MS),从而造成应答失步,故总是提示"通信失败".

    知道这个道理你就不难理解这个电路最上面的二极管和电阻串联起来就是必须要有上电加速电路.这里还用了一只(内部空心不带蓝色的)肖特基二极管(1N5819)从法拉电容向单片机VCC放电,还同时阻断法拉电容对上电加速电路的旁路作用,用肖特基二极管是基于其在小电流下导通电压只有0.2V左右考虑的,目的是尽量减少法拉电容在单片机掉电时的电压损失.多留掉点维持时间。

    三极管9014和钳制位二极管分压电阻垫位电阻(470欧姆)等构成基极上发射极双端输入比较器,实现单片机掉电检测和发出最高优先级的掉电中断,单片机掉电保存程执行。这部分电路相当于半只比较器LM393,但电路更简单耗电更省(掉电时耗电小于0.15MA)。

    47K电阻和470欧姆二极管1N4148一道构成嵌位电路,保证基极电位大约在0.65V左右 (可这样计算0.6(二极管导通电压)+5*0.47/47),这样如果9014发射极电压为0(此时就是外部掉电),三极管9014正好导通,而且因为51单片机P3.2高电平为弱上拉(大约50UA),此时9014一定是导通且弱电流饱和的,这样就向单片机内部发出最高硬件优先级的INX0掉电检测中断。

    而在平时正常供电时,因发射极上也大约有6*0.22/2.2=0.6V电压上顶,不难发现三极管9014一定处于截止状态,而使P3.2维持高电平的,单片机掉电保存中断程序不被触发。

最后还有两个重要软件和硬件note:

软件上

    INX0监测引脚在硬件上(设计)是处于最高优先级的,这里还必须要在软件保证最高级别的优先,从而确保单片机掉电时外部中断0能打断其他任何进程,最高优先地被检测和执行。

硬件上

    凡是驱动单片机外部口线等的以输出高电平驱动外部设备,其电源不能和电片机的供电电压VCC去争抢(例如上拉电阻供电不取自单片机VCC).而应直接接在电源前方,图中4.7K电阻和口线PX.Y就是一个典型示例,接其它口线PX.Y'和负载也雷同。

    这里与上拉4.7K电阻相串联二极管也有两个作用:

  • 钳去0.6V电压以便与单片机工作电压相匹配,防止口线向单片机内部反推电.造成单片机口线功能紊乱。
  • 利用二极管单向供电特性,防止掉电后单片机通过口线向电源和外部设备反供电。

    上面的单片机掉电检测电路,在与掉电保存写入子程序模块结合起来就可以保证在单片机掉电期间,不会因法拉电容上的积累电荷为已经掉电的外部电路无谓供电和向电源反供电造成电容能量泄放缩短掉电维持时间。

    有了这些基础,我们来计算0.47UF的电容从5.5V跌落到4.5V(甚至可以下到3.6V)所能维持的单片机掉电工作时间.这里假设设单片机工作电流为20MA(外设驱动电流已经被屏蔽)不难算出:

T=1V*0.47*1000(1000是因为工作电流为豪安)/20=23.5秒!

二、调试和宏使用技巧

01.调试相关的宏

    在Linux使用gcc编译程序的时候,对于调试的语句还具有一些特殊的语法。gcc编译的过程中,会生成一些宏,可以使用这些宏分别打印当前源文件的信息,主要内容是当前的文件、当前运行的函数和当前的程序行。

    具体宏如下:

__FILE__  当前程序源文件 (char*)
__FUNCTION__  当前运行的函数 (char*)
__LINE__  当前的函数行 (int)

    这些宏不是程序代码定义的,而是有编译器产生的。这些信息都是在编译器处理文件的时候动态产生的。

    测试示例:

#include <stdio.h>

int main(void)
{
    printf("file: %s\n", __FILE__);
    printf("function: %s\n", __FUNCTION__);
    printf("line: %d\n", __LINE__);

    return 0;
}


02.# 字符串化操作符


    在gcc的编译系统中,可以使用#将当前的内容转换成字符串。

    程序示例:

#include <stdio.h>

#define DPRINT(expr) printf("<main>%s = %d\n", #expr, expr);

int main(void)
{
    int x = 3;
    int y = 5;

    DPRINT(x / y);
    DPRINT(x + y);
    DPRINT(x * y);
    
    return 0;
}

    执行结果:

deng@itcast:~/tmp$ gcc test.c 
deng@itcast:~/tmp$ ./a.out  
<main>x / y = 0
<main>x + y = 8
<main>x * y = 15

    #expr表示根据宏中的参数(即表达式的内容),生成一个字符串。该过程同样是有编译器产生的,编译器在编译源文件的时候,如果遇到了类似的宏,会自动根据程序中表达式的内容,生成一个字符串的宏。

    这种方式的优点是可以用统一的方法打印表达式的内容,在程序的调试过程中可以方便直观的看到转换字符串之后的表达式。具体的表达式的内容是什么,有编译器自动写入程序中,这样使用相同的宏打印所有表达式的字符串。

//打印字符
#define debugc(expr) printf("<char> %s = %c\n", #expr, expr)
//打印浮点数
#define debugf(expr) printf("<float> %s = %f\n", #expr, expr)
//按照16进制打印整数
#define debugx(expr) printf("<int> %s = 0X%x\n", #expr, expr);

    由于#expr本质上市一个表示字符串的宏,因此在程序中也可以不适用%s打印它的内容,而是可以将其直接与其它的字符串连接。因此,上述宏可以等价以下形式:

//打印字符
#define debugc(expr) printf("<char> #expr = %c\n", expr)
//打印浮点数
#define debugf(expr) printf("<float> #expr = %f\n", expr)
//按照16进制打印整数
#define debugx(expr) printf("<int> #expr = 0X%x\n", expr);

    总结:

    #是C语言预处理阶段的字符串化操作符,可将宏中的内容转换成字符串。

03.## 连接操作符

    在gcc的编译系统中,##是C语言中的连接操作符,可以在编译的预处理阶段实现字符串连接的操作。

    程序示例:

#include <stdio.h>

#define test(x) test##x

void test1(int a)
{
    printf("test1 a = %d\n", a);
}

void test2(char *s)
{
    printf("test2 s = %s\n", s);
}

int main(void)
{
    test(1)(100);

    test(2)("hello world");
    
    return 0;
}

    上述程序中,test(x)宏被定义为test##x, 他表示test字符串和x字符串的连接。

在程序的调试语句中,##常用的方式如下

#define DEBUG(fmt, args...) printf(fmt, ##args)

    替换的方式是将参数的两个部分以##连接。##表示连接变量代表前面的参数列表。使用这种形式可以将宏的参数传递给一个参数。args…是宏的参数,表示可变的参数列表,使用##args将其传给printf函数.

    总结:

    ##是C语言预处理阶段的连接操作符,可实现宏参数的连接。

04.调试宏第一种形式

    一种定义的方式:

#define DEBUG(fmt, args...)             \
    {                                   \
    printf("file:%s function: %s line: %d ", __FILE__, __FUNCTION__, __LINE__);\
    printf(fmt, ##args);                \
    }

    程序示例:

#include <stdio.h>

#define DEBUG(fmt, args...)             \
    {                                   \
    printf("file:%s function: %s line: %d ", __FILE__, __FUNCTION__, __LINE__);\
    printf(fmt, ##args);                \
    }


int main(void)
{
    int a = 100;
    int b = 200;

    char *s = "hello world";
    DEBUG("a = %d b = %d\n", a, b);
    DEBUG("a = %x b = %x\n", a, b);
    DEBUG("s = %s\n", s);
    
    return 0;
}

    总结:

    上面的DEBUG定义的方式是两条语句的组合,不可能在产生返回值,因此不能使用它的返回值。

05.调试宏的第二种定义方式

    调试宏的第二种定义方式

#define DEBUG(fmt, args...)             \
    printf("file:%s function: %s line: %d "fmt, \
    __FILE__, __FUNCTION__, __LINE__, ##args)

    程序示例

#include <stdio.h>

#define DEBUG(fmt, args...)             \
    printf("file:%s function: %s line: %d "fmt, \
    __FILE__, __FUNCTION__, __LINE__, ##args)


int main(void)
{
    int a = 100;
    int b = 200;

    char *s = "hello world";
    DEBUG("a = %d b = %d\n", a, b);
    DEBUG("a = %x b = %x\n", a, b);
    DEBUG("s = %s\n", s);
    
    return 0;
}

    总结:

    fmt必须是一个字符串,不能使用指针,只有这样才可以实现字符串的功能。

06.对调试语句进行分级审查

    即使定义了调试的宏,在工程足够大的情况下,也会导致在打开宏开关的时候在终端出现大量的信息。而无法区分哪些是有用的。这个时候就要加入分级检查机制,可以定义不同的调试级别,这样就可以对不同重要程序和不同的模块进行区分,需要调试哪一个模块就可以打开那一个模块的调试级别。

    一般可以利用配置文件的方式显示,其实Linux内核也是这么做的,它把调试的等级分成了7个不同重要程度的级别,只有设定某个级别可以显示,对应的调试信息才会打印到终端上。

    可以写出一下配置文件

[debug]
debug_level=XXX_MODULE

    解析配置文件使用标准的字符串操作库函数就可以获取XXX_MODULE这个数值。

int show_debug(int level)
{
    if (level == XXX_MODULE)
    {
        #define DEBUG(fmt, args...)             \
        printf("file:%s function: %s line: %d "fmt, \
        __FILE__, __FUNCTION__, __LINE__, ##args)       
    }
    else if (...)
    {
        ....
    }
}

    在实际的开发中,一般会维护两种源程序,一种是带有调试语句的调试版本程序,另外一种是不带有调试语句的发布版本程序。然后根据不同的条件编译选项,编译出不同的调试版本和发布版本的程序。

    在实现过程中,可以使用一个调试宏来控制调试语句的开关。

#ifdef USE_DEBUG
        #define DEBUG(fmt, args...)             \
        printf("file:%s function: %s line: %d "fmt, \
        __FILE__, __FUNCTION__, __LINE__, ##args)  
#else
  #define DEBUG(fmt, args...)

#endif

    如果USE_DEBUG被定义,那么有调试信息,否则DEBUG就为空。

    如果需要调试信息,就只需要在程序中更改一行就可以了。

#define USE_DEBUG
#undef USE_DEBUG

    定义条件编译的方式使用一个带有值的宏

#if USE_DEBUG
        #define DEBUG(fmt, args...)             \
        printf("file:%s function: %s line: %d "fmt, \
        __FILE__, __FUNCTION__, __LINE__, ##args)  
#else
  #define DEBUG(fmt, args...)

#endif

   可以使用如下方式进行条件编译

#ifndef USE_DEBUG
#define USE_DEBUG 0
#endif


08.使用do…while的宏定义


    使用宏定义可以将一些较为短小的功能封装,方便使用。宏的形式和函数类似,但是可以节省函数跳转的开销。

    如何将一个语句封装成一个宏,在程序中常常使用do…while(0)的形式。

#define HELLO(str) do { \
printf("hello: %s\n", str); \
}while(0)

    程序示例:

int cond = 1;
if (cond)
    HELLO("true");
else
    HELLO("false");

    对于比较大的程序,可以借助一些工具来首先把需要优化的点清理出来。接下来我们来看看在程序执行过程中获取数据并进行分析的工具:代码剖析程序。

    测试程序:

#include <stdio.h>


#define T 100000

void call_one()
{
    int count = T * 1000;
    while(count--);
}

void call_two()
{
    int count = T * 50;
    while(count--);
}

void call_three()
{
    int count = T * 20;
    while(count--);
}


int main(void)
{
    int time = 10;

    while(time--)
    {
        call_one();
        call_two();
        call_three();
    }
    
    return 0;
}

  编译的时候加入-pg选项:

deng@itcast:~/tmp$ gcc -pg  test.c -o test

    执行完成后,在当前文件中生成了一个gmon.out文件。

deng@itcast:~/tmp$ ./test  
deng@itcast:~/tmp$ ls
gmon.out  test  test.c
deng@itcast:~/tmp$

    使用gprof剖析主程序:

deng@itcast:~/tmp$ gprof test
Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
 95.64      1.61     1.61       10   160.68   160.68  call_one
  3.63      1.67     0.06       10     6.10     6.10  call_two
  2.42      1.71     0.04       10     4.07     4.07  call_three

    其中主要的信息有两个,一个是每个函数执行的时间占程序总时间的百分比,另外一个就是函数被调用的次数。通过这些信息,可以优化核心程序的实现方式来提高效率。

    当然这个剖析程序由于它自身特性有一些限制,比较适用于运行时间比较长的程序,因为统计的时间是基于间隔计数这种机制,所以还需要考虑函数执行的相对时间,如果程序执行时间过短,那得到的信息是没有任何参考意义的。

    将上述程序时间缩短:

#include <stdio.h>


#define T 100

void call_one()
{
    int count = T * 1000;
    while(count--);
}

void call_two()
{
    int count = T * 50;
    while(count--);
}

void call_three()
{
    int count = T * 20;
    while(count--);
}


int main(void)
{
    int time = 10;

    while(time--)
    {
        call_one();
        call_two();
        call_three();
    }
    
    return 0;
}

剖析结果如下:

deng@itcast:~/tmp$ gcc -pg test.c -o test
deng@itcast:~/tmp$ ./test  
deng@itcast:~/tmp$ gprof test
Flat profile:

Each sample counts as 0.01 seconds.
 no time accumulated

  %   cumulative   self              self     total           
 time   seconds   seconds    calls  Ts/call  Ts/call  name    
  0.00      0.00     0.00       10     0.00     0.00  call_one
  0.00      0.00     0.00       10     0.00     0.00  call_three
  0.00      0.00     0.00       10     0.00     0.00  call_two

    因此该剖析程序对于越复杂、执行时间越长的函数也适用。

    那么是不是每个函数执行的绝对时间越长,剖析显示的时间就真的越长呢?可以再看如下的例子。

#include <stdio.h>


#define T 100

void call_one()
{
    int count = T * 1000;
    while(count--);
}

void call_two()
{
    int count = T * 100000;
    while(count--);
}

void call_three()
{
    int count = T * 20;
    while(count--);
}


int main(void)
{
    int time = 10;

    while(time--)
    {
        call_one();
        call_two();
        call_three();
    }
    
    return 0;
}

剖析结果如下:

deng@itcast:~/tmp$ gcc -pg test.c -o test
deng@itcast:~/tmp$ ./test  
deng@itcast:~/tmp$ gprof test
Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
101.69      0.15     0.15       10    15.25    15.25  call_two
  0.00      0.15     0.00       10     0.00     0.00  call_one
  0.00      0.15     0.00       10     0.00     0.00  call_three

    总结:

    在使用gprof工具的时候,对于一个函数进行gprof方式的剖析,实质上的时间是指除去库函数调用和系统调用之外,纯碎应用部分开发的实际代码运行的时间,也就是说time一项描述的时间值不包括库函数printf、系统调用system等运行的时间。这些实用库函数的程序虽然运行的时候将比最初的程序实用更多的时间,但是对于剖析函数来说并没有影响。

三、STM32的启动模式配置与应用

3种BOOT模式

    所谓启动,一般来说就是指我们下好程序后,重启芯片时,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存。用户可以通过设置BOOT1和BOOT0引脚的状态,来选择在复位后的启动模式

c语言-嵌入式专辑6~_字符串_02

  • Main Flash memory
    是STM32内置的Flash,一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。
  • System memory
    从系统存储器启动,这种模式启动的程序功能是由厂家设置的。一般来说,这种启动方式用的比较少。系统存储器是芯片内部一块特定的区域,STM32在出厂时,由ST在这个区域内部预置了一段BootLoader, 也就是我们常说的ISP程序, 这是一块ROM,
    出厂后无法修改。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的BootLoader中,提供了串口下载程序的固件,可以通过这个BootLoader将程序下载到系统的Flash中。但是这个下载方式需要以下步骤:
    Step1:将BOOT0设置为1,BOOT1设置为0,然后按下复位键,这样才能从系统存储器启动BootLoader
    Step2:最后在BootLoader的帮助下,通过串口下载程序到Flash中
    Step3:程序下载完成后,又有需要将BOOT0设置为GND,手动复位,这样,STM32才可以从Flash中启动可以看到, 利用串口下载程序还是比较的麻烦, 需要跳帽跳来跳去的,非常的不注重用户体验。
  • Embedded Memory
    内置SRAM,既然是SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。假如我只修改了代码中一个小小的地方,然后就需要重新擦除整个Flash,比较的费时,可以考虑从这个模式启动代码(也就是STM32的内存中),用于快速的程序调试,等程序调试完成后,在将程序下载到SRAM中。

开发BOOT模式选择

    通常使用程序代码存储在主闪存存储器,配置方式:BOOT0=0,BOOT1=X。

Flash锁死解决办法

    开发调试过程中,由于某种原因导致内部Flash锁死,无法连接SWD以及Jtag调试,无法读到设备,可以通过修改BOOT模式重新刷写代码。

    修改为BOOT0=1,BOOT1=0即可从系统存储器启动,ST出厂时自带Bootloader程序,SWD以及JTAG调试接口都是专用的。重新烧写程序后,可将BOOT模式重新更换到BOOT0=0,BOOT1=X即可正常使用。

四、单片机开发中,传感器的数据处理算法

 在传感器使用中,我们常常需要对传感器数据进行各种整理,让应用获得更好的效果,以下介绍几种常用的简单处理方法:

  • 加权平滑:平滑和均衡传感器数据,减小偶然数据突变的影响。
  • 抽取突变:去除静态和缓慢变化的数据背景,强调瞬间变化。
  • 简单移动平均线:保留数据流最近的K个数据,取平均值。

    下面,具体介绍一下这3种处理方法。

加权平滑

    使用算法如下:

    (新值) = (旧值)*(1 - a) + X * a其中a为设置的权值,X为最新数据,程序实现如下:

c语言-嵌入式专辑6~_字符串_03

抽取突变

    此算法采用上面加权平滑的逆算法实现代码如下:

c语言-嵌入式专辑6~_字符串_04

简单移动平均线

    这个算法,保留传感器数据流中最近的K个数据,返回它们的平均值。k表示平均“窗口”的大小,实现代码如下:

c语言-嵌入式专辑6~_c语言_05

c语言-嵌入式专辑6~_c语言_06

c语言-嵌入式专辑6~_字符串_07

五、STM32标准库、HAL库特点与应用

  新手在入门STM32的时候,一般大多数都会选用标准库和HAL库,而极少部分人会通过直接配置寄存器进行开发。

    对于刚入门的朋友可能没法直观了解这些不同开发发方式彼此之间的区别,本文试图以一种非常直白的方式,用自己的理解去将这些东西表述出来。

配置寄存器

    不少先学了51的朋友可能会知道,会有一小部分人或是教程是通过汇编语言直接操作寄存器实现功能的,这种方法到了STM32就变得不太容易行得通了。

    因为STM32的寄存器数量是51单片机的十数倍,如此多的寄存器根本无法全部记忆,开发时需要经常的翻查芯片的数据手册,此时直接操作寄存器就变得非常的费力了。也有人喜欢去直接操作寄存器,因为这样更接近原理,代码更少,知其然也知其所以然。

标准库

    上面也提到了,STM32有非常多的寄存器,而导致了开发困难,所以为此ST公司就为每款芯片都编写了一份库文件,也就是工程文件里stm32F1xx.....之类的。在这些.c与.h文件中,包括一些常用量的宏定义,把一些外设也通过结构体变量封装起来,如GPIO、时钟等。

    所以我们只需要配置结构体变量成员就可以修改外设的配置寄存器,从而选择不同的功能。也是目前最多人使用的方式,也是学习STM32接触最多的一种开发方式,我也就不多阐述了。

HAL库

    HAL库是ST公司目前主推的开发方式,全称就是Hardware Abstraction Layer(抽象印象层),简单来说就是弱化了开发者对硬件底层知识的依赖。

    同样的功能,标准库可能要用几句话,HAL库只需用一句话就够了。并且HAL库也很好的解决了程序移植的问题。不同型号的STM32芯片它的标准库是不一样的,例如在F4上开发的程序移植到F3上是不能通用的,而使用HAL库,只要使用的是相同的外设,程序基本可以完全复制粘贴,注意是相同外设,意思也就是不能无中生有。例如F7比F3要多几个定时器,不能明明没有这个定时器却非要配置,但其实这种情况不多,绝大多数都可以直接复制粘贴。

    而且使用ST公司研发的STMcube软件,可以通过图形化的配置功能,直接生成整个适用于HAL库的工程文件,可以说是方便至极。相关推荐:STM32CubeMX安装教程。但是方便的同时也造成了它执行效率偏低。

    综合上面说的,其实笔者还是强烈推荐HAL库的,理由:

  • ST公司已经停止更新标准库,公司主打HAL库的目的已经非常明显了
  • 模块化的HAL库是趋势,低效的短板会被硬件的增强所弥补

    当然底层的基本原理必需是要懂的,HAL库也不是万能的,结合对底层的理解相信一定会让你的开发水准大大提高。

STM32的HAL库、标准库区别

1 句柄

    在STM32的标准库中,假设我们要初始化一个外设(这里以USART为例) 我们首先要初始化他们的各个寄存器。

    在标准库中,这些操作都是利用固件库结构体变量+固件库Init函数实现的:

USART_InitTypeDef USART_InitStructure;

 USART_InitStructure.USART_BaudRate = bound;//串口波特率
 USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
 USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
 USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式

 USART_Init(USART3, &USART_InitStructure); //初始化串口1

    可以看到,要初始化一个串口,需要对六个位置进行赋值,然后引用Init函数,并且USART_InitStructure并不是一个全局结构体变量,而是只在函数内部的局部变量,初始化完成之后,USART_InitStructure就失去了作用。

    而在HAL库中,同样是USART初始化结构体变量,我们要定义为全局变量。

UART_HandleTypeDef UART1_Handler;

    结构体成员:

typedef struct
{
   USART_TypeDef                 *Instance;        /*!< UART registers base address        */
   UART_InitTypeDef              Init;             /*!< UART communication parameters      */
uint8_t                       *pTxBuffPtr;      /*!< Pointer to UART Tx transfer Buffer */
uint16_t                      TxXferSize;       /*!< UART Tx Transfer size              */
uint16_t                      TxXferCount;      /*!< UART Tx Transfer Counter           */
uint8_t                       *pRxBuffPtr;      /*!< Pointer to UART Rx transfer Buffer */
uint16_t                      RxXferSize;       /*!< UART Rx Transfer size              */
uint16_t                      RxXferCount;      /*!< UART Rx Transfer Counter           */
   DMA_HandleTypeDef             *hdmatx;          /*!< UART Tx DMA Handle parameters      */
   DMA_HandleTypeDef             *hdmarx;          /*!< UART Rx DMA Handle parameters      */
   HAL_LockTypeDef               Lock;             /*!< Locking object                     */
   __IO HAL_UART_StateTypeDef    State;            /*!< UART communication state           */
   __IO uint32_t                 ErrorCode;        /*!< UART Error code                    */
}UART_HandleTypeDef;

    我们发现,与标准库不同的是,该成员不仅包含了之前标准库就有的六个成员(波特率,数据格式等),还包含过采样、(发送或接收的)数据缓存、数据指针、串口 DMA 相关的变量、各种标志位等等要在整个项目流程中都要设置的各个成员。

    该UART1_Handler就被称为串口的句柄 它被贯穿整个USART收发的流程,比如开启中断:

HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);

    比如后面要讲到的MSP与Callback回调函数:

void HAL_UART_MspInit(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);

    在这些函数中,只需要调用初始化时定义的句柄UART1_Handler就好。

2 MSP函数

    MCU Specific Package单片机的具体方案

    MSP是指和MCU相关的初始化,引用一下正点原子的解释,个人觉得说的很明白:

我们要初始化一个串口,首先要设置和 MCU 无关的东西,例如波特率,奇偶校验,停止位等,这些参数设置和 MCU 没有任何关系,可以使用 STM32F1,也可以是STM32F2/F3/F4/F7上的串口。而一个串口设备它需要一个 MCU 来承载,例如用 STM32F4 来做承载,PA9 做为发送,PA10 做为接收,MSP 就是要初始化 STM32F4 的 PA9,PA10,配置这两个引脚。所以 HAL驱动方式的初始化流程就是:HAL_USART_Init()—>HAL_USART_MspInit(),先初始化与 MCU无关的串口协议,再初始化与 MCU 相关的串口引脚。在 STM32 的 HAL 驱动中HAL_PPP_MspInit()作为回调,被HAL_PPP_Init()函数所调用。当我们需要移植程序到 STM32F1平台的时候,我们只需要修改 HAL_PPP_MspInit 函数内容而不需要修改 HAL_PPP_Init 入口参数内容。

    在HAL库中,几乎每初始化一个外设就需要设置该外设与单片机之间的联系,比如IO口,是否复用等等,可见,HAL库相对于标准库多了MSP函数之后,移植性非常强,但与此同时却增加了代码量和代码的嵌套层级。可以说各有利弊。

    同样,MSP函数又可以配合句柄,达到非常强的移植性:

void HAL_UART_MspInit(UART_HandleTypeDef *huart);

3 Callback函数

    类似于MSP函数,个人认为Callback函数主要帮助用户应用层的代码编写。还是以USART为例,在标准库中,串口中断了以后,我们要先在中断中判断是否是接收中断,然后读出数据,顺便清除中断标志位,然后再是对数据的处理,这样如果我们在一个中断函数中写这么多代码,就会显得很混乱:

void USART3_IRQHandler(void) //串口1中断服务程序
{
 u8 Res;
if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
 {
  Res =USART_ReceiveData(USART3); //读取接收到的数据
/*数据处理区*/
 } 
}

    而在HAL库中,进入串口中断后,直接由HAL库中断函数进行托管:

void USART1_IRQHandler(void)                 
{ 
 HAL_UART_IRQHandler(&UART1_Handler); //调用HAL库中断处理公用函数
 /***************省略无关代码****************/ 
}

    HAL_UART_IRQHandler这个函数完成了判断是哪个中断(接收?发送?或者其他?),然后读出数据,保存至缓存区,顺便清除中断标志位等等操作。比如我提前设置了,串口每接收五个字节,我就要对这五个字节进行处理。在一开始我定义了一个串口接收缓存区:

/*HAL库使用的串口接收缓冲,处理逻辑由HAL库控制,接收完这个数组就会调用HAL_UART_RxCpltCallback进行处理这个数组*/
/*RXBUFFERSIZE=5*/
u8 aRxBuffer[RXBUFFERSIZE];

    在初始化中,我在句柄里设置好了缓存区的地址,缓存大小(五个字节)

/*该代码在HAL_UART_Receive_IT函数中,初始化时会引用*/
huart->pRxBuffPtr = pData;//aRxBuffer
huart->RxXferSize = Size;//RXBUFFERSIZE
huart->RxXferCount = Size;//RXBUFFERSIZE

    则在接收数据中,每接收完五个字节,HAL_UART_IRQHandler才会执行一次Callback函数:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);

    在这个Callback回调函数中,我们只需要对这接收到的五个字节(保存在aRxBuffer[]中)进行处理就好了,完全不用再去手动清除标志位等操作。

    所以说Callback函数是一个应用层代码的函数,我们在一开始只设置句柄里面的各个参数,然后就等着HAL库把自己安排好的代码送到手中就可以了~

    综上,就是HAL库的三个与标准库不同的地方之个人见解。

    个人觉得从这三个小点就可以看出HAL库的可移植性之强大,并且用户可以完全不去理会底层各个寄存器的操作,代码也更有逻辑性。但与此带来的是复杂的代码量,极慢的编译速度,略微低下的效率。看怎么取舍了。

HAL库结构

    说到STM32的HAL库,就不得不提STM32CubeMX,其作为一个可视化的配置工具,对于开发者来说,确实大大节省了开发时间。另外STM32CubeIDE集成了STM32CubeMX的功能,是一个集配置与编译与一体的软件,可以尝试一下。软件更新频率很高,持续优化某些bug及性能问题。

c语言-嵌入式专辑6~_单片机_08

  上面两个开发软件是以HAL库为基础的,且目前仅支持HAL库及LL库!

    首先看一下,官方给出的HAL库的文件结构:

c语言-嵌入式专辑6~_单片机_09

    下图是STM32库文件结构。

c语言-嵌入式专辑6~_字符串_10

c语言-嵌入式专辑6~_#define_11

 stm32f2xx.h 主要包含STM32同系列芯片的不同具体型号的定义,是否使用HAL库等的定义,接着,其会根据定义的芯片信号包含具体的芯片型号的头文件:

#if defined(STM32F205xx)
#include "stm32f205xx.h"
#elif defined(STM32F215xx)
#include "stm32f215xx.h"
#elif defined(STM32F207xx)
#include "stm32f207xx.h"
#elif defined(STM32F217xx)
#include "stm32f217xx.h"
#else
#error "Please select first the target STM32F2xx device used in your application (in stm32f2xx.h file)"
#endif

紧接着,其会包含 stm32f2xx_hal.h。

  • stm32f2xx_hal.h:stm32f2xx_hal.c/h 主要实现HAL库的初始化、系统滴答相关函数、及CPU的调试模式配置
  • stm32f2xx_hal_conf.h :该文件是一个用户级别的配置文件,用来实现对HAL库的裁剪,其位于用户文件目录,不要放在库目录中。

    接下来对于HAL库的源码文件进行一下说明,HAL库文件名均以stm32f2xx_hal开头,后面加上_外设或者模块名(如:stm32f2xx_hal_adc.c):

库文件:
 stm32f2xx_hal_ppp.c/.h   // 主要的外设或者模块的驱动源文件,包含了该外设的通用API
 stm32f2xx_hal_ppp_ex.c/.h  // 外围设备或模块驱动程序的扩展文件。这组文件中包含特定型号或者系列的芯片的特殊API。以及如果该特定的芯片内部有不同的实现方式,则该文件中的特殊API将覆盖_ppp中的通用API。
 stm32f2xx_hal.c/.h    // 此文件用于HAL初始化,并且包含DBGMCU、重映射和基于systick的时间延迟等相关的API
 其他库文件
用户级别文件:
 stm32f2xx_hal_msp_template.c // 只有.c没有.h。它包含用户应用程序中使用的外设的MSP初始化和反初始化(主程序和回调函数)。使用者复制到自己目录下使用模板。
 stm32f2xx_hal_conf_template.h // 用户级别的库配置文件模板。使用者复制到自己目录下使用
 system_stm32f2xx.c    // 此文件主要包含SystemInit()函数,该函数在刚复位及跳到main之前的启动过程中被调用。**它不在启动时配置系统时钟(与标准库相反)**。时钟的配置在用户文件中使用HAL API来完成。
 startup_stm32f2xx.s    // 芯片启动文件,主要包含堆栈定义,终端向量表等
 stm32f2xx_it.c/.h    // 中断处理函数的相关实现
 main.c/.h

 根据HAL库的命名规则,其API可以分为以下三大类:

  • 初始化/反初始化函数:HAL_PPP_Init(), HAL_PPP_DeInit()
  • IO 操作函数:HAL_PPP_Read(), HAL_PPP_Write(),HAL_PPP_Transmit(), HAL_PPP_Receive()
  • 控制函数:HAL_PPP_Set (), HAL_PPP_Get ().
  • 状态和错误:HAL_PPP_GetState (), HAL_PPP_GetError ().

注意:目前 LL 库是和 HAL 库捆绑发布的,所以在 HAL 库源码中,还有一些名为 stm32f2xx_ll_ppp 的源码文件,这些文件就是新增的LL库文件。使用 CubeMX 生产项目时,可以选择LL库。

    HAL 库最大的特点就是对底层进行了抽象。在此结构下,用户代码的处理主要分为三部分:

  • 处理外设句柄,实现用户功能
  • 处理MSP
  • 处理各种回调函数,外设句柄定义

    HAL库在结构上,对每个外设抽象成了一个称为ppp_HandleTypeDef的结构体,其中ppp就是每个外设的名字。*所有的函数都是工作在ppp_HandleTypeDef指针之下。

    每个外设/模块实例都有自己的句柄。因此,实例资源是独立的。

    外围进程相互通信:该句柄用于管理进程例程之间的共享数据资源。

    下面,以ADC为例,

/** 
 * @brief  ADC handle Structure definition
 */
typedef struct
{
 ADC_TypeDef                   *Instance;                   /*!< Register base address */
 ADC_InitTypeDef               Init;                        /*!< ADC required parameters */
  __IO uint32_t                 NbrOfCurrentConversionRank;  /*!< ADC number of current conversion rank */
 DMA_HandleTypeDef             *DMA_Handle;                 /*!< Pointer DMA Handler */
 HAL_LockTypeDef               Lock;                        /*!< ADC locking object */
 __IO uint32_t                 State;                       /*!< ADC communication state */
 __IO uint32_t                 ErrorCode;                   /*!< ADC Error code */
}ADC_HandleTypeDef;

从上面的定义可以看出,ADC_HandleTypeDef中包含了ADC可能出现的所有定义,对于用户想要使用ADC只要定义一个ADC_HandleTypeDef的变量,给每个变量赋好值,对应的外设就抽象完了。接下来就是具体使用了。

    当然,对于那些共享型外设或者说系统外设来说,他们不需要进行以上这样的抽象,这些部分与原来的标准外设库函数基本一样。例如以下外设:

  • GPIO
  • SYSTICK
  • NVIC
  • RCC
  • FLASH

    以GPIO 为例,对于HAL_GPIO_Init()函数,其只需要GPIO地址以及其初始化参数即可。

1 三种编程方式

    HAL库对所有的函数模型也进行了统一。在HAL库中,支持三种编程模式:轮询模式、中断模式、DMA模式(如果外设支持)。其分别对应如下三种类型的函数(以ADC为例):

HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef* hadc);

HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_Stop_IT(ADC_HandleTypeDef* hadc);

HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length);
HAL_StatusTypeDef HAL_ADC_Stop_DMA(ADC_HandleTypeDef* hadc);

    其中,带_IT的表示工作在中断模式下;带_DMA的工作在DMA模式下(注意:DMA模式下也是开中断的);什么都没带的就是轮询模式(没有开启中断的)。至于使用者使用何种方式,就看自己的选择了。

    此外,新的HAL库架构下统一采用宏的形式对各种中断等进行配置(原来标准外设库一般都是各种函数)。针对每种外设主要由以下宏:

  • __HAL_PPP_ENABLE_IT(HANDLE, INTERRUPT):使能一个指定的外设中断
  • __HAL_PPP_DISABLE_IT(HANDLE, INTERRUPT):失能一个指定的外设中断
  • __HAL_PPP_GET_IT (HANDLE, __ INTERRUPT __):获得一个指定的外设中断状态
  • __HAL_PPP_CLEAR_IT (HANDLE, __ INTERRUPT __):清除一个指定的外设的中断状态
  • __HAL_PPP_GET_FLAG (HANDLE, FLAG):获取一个指定的外设的标志状态
  • __HAL_PPP_CLEAR_FLAG (HANDLE, FLAG):清除一个指定的外设的标志状态
  • __HAL_PPP_ENABLE(HANDLE) :使能外设
  • __HAL_PPP_DISABLE(HANDLE) :失能外设
  • __HAL_PPP_XXXX (HANDLE, PARAM) :指定外设的宏定义
  • _HAL_PPP_GET IT_SOURCE (HANDLE, __ INTERRUPT __)检查中断源

2 三大回调函数

    在 HAL 库的源码中,到处可见一些以__weak开头的函数,而且这些函数,有些已经被实现了,比如:

__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
/*Configure the SysTick to have interrupt in 1ms time basis*/
 HAL_SYSTICK_Config(SystemCoreClock/1000U);
/*Configure the SysTick IRQ priority */
 HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority ,0U);
/* Return function status */
return HAL_OK;
}

有些则没有被实现,例如:

__weak void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
/* Prevent unused argument(s) compilation warning */
  UNUSED(hspi);
/* NOTE : This function should not be modified, when the callback is needed,the HAL_SPI_TxCpltCallback should be implemented in the user file
  */
}

  所有带有__weak关键字的函数表示,就可以由用户自己来实现。如果出现了同名函数,且不带__weak关键字,那么连接器就会采用外部实现的同名函数。通常来说,HAL库负责整个处理和MCU外设的处理逻辑,并将必要部分以回调函数的形式给出到用户,用户只需要在对应的回调函数中做修改即可。

    HAL 库包含如下三种用户级别回调函数(PPP为外设名):

  1. 外设系统级初始化/解除初始化回调函数(用户代码的第二大部分:对于MSP的处理):HAL_PPP_MspInit()HAL_PPP_MspDeInit 例如:__weak void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)。在HAL_PPP_Init() 函数中被调用,用来初始化底层相关的设备(GPIOs, clock, DMA, interrupt)
  2. 处理完成回调函数:HAL_PPP_ProcessCpltCallback(Process指具体某种处理,如UART的Tx),例如:__weak void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)。当外设或者DMA工作完成后时,触发中断,该回调函数会在外设中断处理函数或者DMA的中断处理函数中被调用
  3. 错误处理回调函数:HAL_PPP_ErrorCallback例如:__weak void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)。当外设或者DMA出现错误时,触发终端,该回调函数会在外设中断处理函数或者DMA的中断处理函数中被调用。