单片机

在编写单片机程序的时候,由于中断服务程序写的不好,导致单片机程序总是跑飞,最后费了好长时间,花了很大功夫才找到问题原因,由此总结了单片机程序跑飞的三种现象、原因及解决方法。

1、数组越界/溢出

现象:

单片机程序在函数中运行时,总是在运行到函数末尾,要跳出函数时,程序跑飞。

原因:

数组越界(数组溢出),函数中定义的数组元素的个数小于程序中实际使用的数组元素的个数,例如在函数中定义了一个数组ucDataBuff[10],这个数组只有10个元素,但是在函数中却有这样的语句ucDataBuff[10]=0x1a,这个语句是给数组的第11个元素赋值,:由于定义的数组只有10个元素,从而导致赋值语句中不知道把0x1a放到什么地方,从而导致程序跑飞。

解决方法:

如果在调试程序时,发现程序总是在函数执行完毕时跑飞,多数情况是发生了数组越界(数组溢出)的错误,仔细检查函数中调用的数组是否存在越界(溢出)的情况。

2、中断服务程序缺失

现象:

程序运行过程中总是跑飞。

原因:

程序中打开了某个中断,但是却没有相应的中断服务程序,从而导致在中断发生后,找不到中断服务程序入口,从而导致程序跑飞。

解决方法:

检查程序中是否存在打开了某个中断,但是没有相对应的中断服务程序。

3、看门狗复位

现象:

在执行一段较为耗费时间的程序时,程序跑飞,并且总是跳到复位位置处。

原因:

程序中使用了看门狗,但是没有及时“喂狗”,从而导致看门狗复位,使程序直接跳到复位位置。

解决方法:

根据程序运行时间,尤其是一定要计算清楚最耗时的那段程序的运行时间,然后准确设置看门狗的复位时长,定时“喂狗”,尤其是如果有死循环的情况,一定要在死循环中记得“喂狗”。

来源:网络
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 32

三极管在数字电路里的开关特性,最常见的应用有 2 个:一个是控制应用,一个是驱动应用。所谓的控制就是,我们可以通过单片机控制三极管的基极来间接控制后边的小灯的亮灭,用法大家基本熟悉了。

还有一个控制就是进行不同电压之间的转换控制,比如我们的单片机是 5V 系统,它现在要跟一个 12V 的系统对接,如果 IO 直接接 12V电压就会烧坏单片机,所以我们加一个三极管,三极管的工作电压高于单片机的 IO 口电压,用 5V 的 IO 口来控制 12V 的电路,如图 1 所示。

“图1
图1 三极管实现电压转换

图1中,当 IO 口输出高电平 5V 时,三极管导通,OUT 输出低电平 0V,当 IO 口输出低电平时,三极管截止,OUT 则由于上拉电阻 R2 的作用而输出 12V 的高电平,这样就实现了低电压控制高电压的工作原理。

所谓的驱动,主要是指电流输出能力。我们再来看如图2中两个电路之间的对比。

“图2
图2 LED 小灯控制方式对比

图2中上边的 LED 灯,和我们第二课讲过的 LED 灯是一样的,当 IO 口是高电平时,小灯熄灭,当 IO 口是低电平时,小灯点亮。那么下边的电路呢,按照这种推理,IO 口是高电平的时候,应该有电流流过并且点亮小灯,但实际上却并非这么简单。

单片机主要是个控制器件,具备四两拨千斤的特点。就如同杠杆必须有一个支点一样,想要撑起整个地球必须有力量承受的支点。

单片机的 IO 口可以输出一个高电平,但是他的输出电流却很有限,普通 IO 口输出高电平的时候,大概只有几十到几百 uA 的电流,达不到1mA,也就点不亮这个 LED 小灯或者是亮度很低,这个时候如果我们想用高电平点亮 LED,就可以用上三极管来处理了,我们板上的这种三极管型号,可以通过 500mA 的电流,有的三极管通过的电流还更大一些,如图 3所示。

“图3
图3 三极管驱动 LED 小灯

图3中,当 IO 口是高电平,三极管导通,因为三极管的电流放大作用,c 极电流就可以达到 mA 以上了,就可以成功点亮 LED 小灯。

虽然我们用了 IO 口的低电平可以直接点亮 LED,但是单片机的 IO 口作为低电平,输入电流就可以很大吗?

这个我想大家都能猜出来,当然不可以。单片机的 IO 口电流承受能力,不同型号不完全一样,就 STC89C52 来说,官方手册的 81 页有对电气特性的介绍,整个单片机的工作电流,不要超过 50mA,单个 IO 口总电流不要超过 6mA。

即使一些增强型 51 的IO 口承受电流大一点,可以到 25mA,但是还要受到总电流 50mA 的限制。那我们来看电路图的 8 个 LED 小灯这部分电路,如图4所示。

“图4
图4 LED 电路图(一)

这里我们要学会看电路图的一个知识点,电路图右侧所有的 LED 下侧的线最终都连到一根黑色的粗线上去了,大家注意,这个地方不是实际的完全连到一起,而是一种总线的画法,画了这种线以后,表示这是个总线结构。

而所有的名字一样的节点是一一对应的连接到一起,其他名字不一样的,是不连在一起的。比如左侧的 DB0 和右侧的最右边的 LED2 小灯下边的DB0 是连在一起的,而和 DB1 等其他线不是连在一起的。

那么我们把图4中现在需要讲解的这部分单独摘出来看,如图5所示。

“图5
图5 LED 电路图(二)

现在我们通过5的电路图来计算一下,5V 的电压减去 LED 本身的压降,减掉三极管e 和 c 之间的压降,限流电阻用的是 330 欧,那么每条支路的电流大概是 8mA,那么 8 路 LED如果全部同时点亮的话电流总和就是 64mA。这样如果直接接到单片机的 IO 口,那单片机肯定是承受不了的,即使短时间可以承受,长时间工作就会不稳定,甚至导致单片机烧毁。

有的同学会提出来可以加大限流电阻的方式来降低这个电流。比如改到 1K,那么电流不到 3mA,8 路总的电流就是 20mA 左右。

首先,降低电流会导致 LED 小灯亮度变暗,小灯的亮度可能关系还不大,但因为我们同样的电路接了数码管,后边我们要讲数码管还要动态显示,如果数码管亮度不够的话,那视觉效果就会很差,所以降低电流的方法并不可取。

其次,对于单片机来说,他主要是起到控制作用,电流输入和输出的能力相对较弱,P0 的 8 个口总电流也有一定限制,所以如果接一两个 LED 小灯观察,可以勉强直接用单片机的 IO 口来接,但是接多个小灯,从实际工程的角度去考虑,就不推荐直接接 IO 口了。那么我们如果要用单片机控制多个 LED 小灯该怎么办呢?

除了三极管之外,其实还有一些驱动 IC,这些驱动 IC 可以作为单片机的缓冲器,仅仅是电流驱动缓冲,不起到任何逻辑控制的效果,比如我们板子上用的 74HC245 这个芯片,这个芯片在逻辑上起不到什么别的作用,就是当做电流缓冲器的,我们通过查看其数据手册,74HC245 稳定工作在 70mA 电流是没有问题的,比单片机的 8 个 IO 口大多了,所以我们可以把他接在小灯和 IO 口之间做缓冲,如图6所示。

“图6
图6 74HC245 功能图

从图6我们来分析,其中 VCC 和 GND 就不用多说了,细心的同学会发现这里有个0.1uF 的去耦电容哦。

74HC245 是个双向缓冲器,1 引脚 DIR 是方向引脚,当这个引脚接高电平的时候,右侧所有的 B 编号的电压都等于左侧 A 编号对应的电压。比如 A1 是高电平,那么 B1 就是高电平,A2 是低电平,B2 就是低电平等等。如果 DIR 引脚接低电平,得到的效果是左侧 A 编号的电压都会等于右侧 B 编号对应的电压。

因为我们这个地方控制端是左侧接的是 P0 口,我们要求 B 等于 A 的状态,所以 1 脚我们直接接的 5V 电源,即高电平。图 3-13 中还有一排电阻 R10 到 R17 是上拉电阻,这个电阻的用法我们在后边介绍。

还有最后一个使能引脚 19 脚 OE,叫做输出使能,这个引脚上边有一横,表明是低电平有效,当接了低电平后,74HC245 就会按照刚才上边说的起到双向缓冲器的作用,如果 OE接了高电平,那么无论 DIR 怎么接,A 和 B 的引脚是没有关系的,也就是 74HC245 功能不能实现出来。

从下面的图7可以看出来,单片机的 P0 口和 74HC245 的 A 端是直接接起来的。

这个地方,有个别同学有个疑问,就是我们明明在电源 VCC 那地方加了一个三极管驱动了,为何还要再加 245 驱动芯片呢?

这里大家要理解一个道理,电路上从正极经过器件到地,首先必须有电流才能正常工作,电路中任何一个位置断开,都不会有电流,器件也就不会参与工作了。

其次,和水流一个道理,从电源正极到负极的电流水管的粗细都要满足要求,任何一个位置的管子过细,都会出现瓶颈效应,电流在整个通路中细管处会受到限制而降低,所以在电路通路的每个位置上,都要保证通道足够畅通,这个 74HC245 的作用就是消除单片机IO 这一环节的瓶颈。

“图7
图7 单片机与 74HC245 的连接

本文转载自:网络
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 11

今天分享一篇单片机程序框架的文章。

程序架构重要性

很多人尤其是初学者在写代码的时候往往都是想一点写一点,最开始没有一个整体的规划,导致后面代码越写越乱,bug不断。

最终代码跑起来看似没有问题(有可能也真的没有问题),但是要加一个功能的时候会浪费大量的时间,甚至导致整个代码的崩溃。

所以,在一个项目开始的时候多花一些时间在代码的架构设计上是十分有必要的。代码架构确定好了之后你会发现敲代码的时候会特别快,并且在后期调试的时候也不会像无头苍蝇一样胡乱找问题。当然,调试也是一门技术。

在学习实时操作系统的过程中,发现实时操作系统框架与个人的业务代码之间的耦合性就非常低,都是只需要将业务代码通过一定的接口函数注册好后就交给操作系统托管了,十分方便。

但是操作系统的调度过于复杂,这里就使用操作系统的思维方式来重构这个时间片轮询框架。实现该框架的完全解耦,用户只需要包含头文件,并且在使用过程中不需要改动已经写好的库文件。

Demo

首先来个demo,该demo是使用电脑开两个线程:一个线程模拟单片机的定时器中断产生时间片轮询个时钟,另一个线程则模拟主函数中一直运行的时间片轮询调度程序。

#include <thread>
#include <stdio.h>
#include <windows.h>
#include "timeslice.h"

// 创建5个任务对象
TimesilceTaskObj task_1, task_2, task_3, task_4, task_5;

// 具体的任务函数
void task1_hdl()
{
    printf(">> task 1 is running ...\n");
}

void task2_hdl()
{
    printf(">> task 2 is running ...\n");
}

void task3_hdl()
{
    printf(">> task 3 is running ...\n");
}

void task4_hdl()
{
    printf(">> task 4 is running ...\n");
}

void task5_hdl()
{
    printf(">> task 5 is running ...\n");
}

// 初始化任务对象,并且将任务添加到时间片轮询调度中
void task_init()
{
    timeslice_task_init(&task_1, task1_hdl, 1, 10);
    timeslice_task_init(&task_2, task2_hdl, 2, 20);
    timeslice_task_init(&task_3, task3_hdl, 3, 30);
    timeslice_task_init(&task_4, task4_hdl, 4, 40);
    timeslice_task_init(&task_5, task5_hdl, 5, 50);
    timeslice_task_add(&task_1);
    timeslice_task_add(&task_2);
    timeslice_task_add(&task_3);
    timeslice_task_add(&task_4);
    timeslice_task_add(&task_5);
}


// 开两个线程模拟在单片机上的运行过程
void timeslice_exec_thread()
{
    while (true)
    {
        timeslice_exec();
    }
}

void timeslice_tick_thread()
{
    while (true)
    {
        timeslice_tick();
        Sleep(10);
    }
}

int main()
{
    task_init();

    printf(">> task num: %d\n", timeslice_get_task_num());
    printf(">> task len: %d\n", timeslice_get_task_timeslice_len(&task_3));

    timeslice_task_del(&task_2);
    printf(">> delet task 2\n");
    printf(">> task 2 is exist: %d\n", timeslice_task_isexist(&task_2));

    printf(">> task num: %d\n", timeslice_get_task_num());

    timeslice_task_del(&task_5);
    printf(">> delet task 5\n");

    printf(">> task num: %d\n", timeslice_get_task_num());

    printf(">> task 3 is exist: %d\n", timeslice_task_isexist(&task_3));
    timeslice_task_add(&task_2);
    printf(">> add task 2\n");
    printf(">> task 2 is exist: %d\n", timeslice_task_isexist(&task_2));

    timeslice_task_add(&task_5);
    printf(">> add task 5\n");

    printf(">> task num: %d\n", timeslice_get_task_num());

    printf("\n\n========timeslice running===========\n");

    std::thread thread_1(timeslice_exec_thread);
    std::thread thread_2(timeslice_tick_thread);

    thread_1.join();
    thread_2.join();


    return 0;
}

运行结果如下:

“单片机面向对象思维的架构:时间轮片法"

由以上例子可见,这个框架使用十分方便,甚至可以完全不知道其原理,仅仅通过几个简单的接口就可以迅速创建任务并加入到时间片轮询的框架中,十分好用。

时间片轮询架构

其实该部分主要使用了面向对象的思维,使用结构体作为对象,并使用结构体指针作为参数传递,这样作可以节省资源,并且有着极高的运行效率。

其中最难的部分是侵入式链表的使用,这种链表在一些操作系统内核中使用十分广泛,这里是参考RT-Thread实时操作系统中的侵入式链表实现。

h文件:

#ifndef _TIMESLICE_H
#define _TIMESLICE_H

#include "./list.h"

typedef enum {
    TASK_STOP,
    TASK_RUN
} IsTaskRun;

typedef struct timesilce
{
    unsigned int id;
    void (*task_hdl)(void);
    IsTaskRun is_run;
    unsigned int timer;
    unsigned int timeslice_len;
    ListObj timeslice_task_list;
} TimesilceTaskObj;

void timeslice_exec(void);
void timeslice_tick(void);
void timeslice_task_init(TimesilceTaskObj* obj, void (*task_hdl)(void), unsigned int id, unsigned int timeslice_len);
void timeslice_task_add(TimesilceTaskObj* obj);
void timeslice_task_del(TimesilceTaskObj* obj);
unsigned int timeslice_get_task_timeslice_len(TimesilceTaskObj* obj);
unsigned int timeslice_get_task_num(void);
unsigned char timeslice_task_isexist(TimesilceTaskObj* obj);

#endif

c文件:

#include "./timeslice.h"

static LIST_HEAD(timeslice_task_list);

void timeslice_exec()
{
    ListObj* node;
    TimesilceTaskObj* task;

    list_for_each(node, &timeslice_task_list)
    {
           
        task = list_entry(node, TimesilceTaskObj, timeslice_task_list);
        if (task->is_run == TASK_RUN)
        {
            task->task_hdl();
            task->is_run = TASK_STOP;
        }
    }
}

void timeslice_tick()
{
    ListObj* node;
    TimesilceTaskObj* task;

    list_for_each(node, &timeslice_task_list)
    {
        task = list_entry(node, TimesilceTaskObj, timeslice_task_list);
        if (task->timer != 0)
        {
            task->timer--;
            if (task->timer == 0)
            {
                task->is_run = TASK_RUN;
                task->timer = task->timeslice_len;
            }
        }
    }
}

unsigned int timeslice_get_task_num()
{
    return list_len(&timeslice_task_list);
}

void timeslice_task_init(TimesilceTaskObj* obj, void (*task_hdl)(void), unsigned int id, unsigned int timeslice_len)
{
    obj->id = id;
    obj->is_run = TASK_STOP;
    obj->task_hdl = task_hdl;
    obj->timer = timeslice_len;
    obj->timeslice_len = timeslice_len;
}

void timeslice_task_add(TimesilceTaskObj* obj)
{
    list_insert_before(&timeslice_task_list, &obj->timeslice_task_list);
}

void timeslice_task_del(TimesilceTaskObj* obj)
{
    if (timeslice_task_isexist(obj))
        list_remove(&obj->timeslice_task_list);
    else
        return;
}


unsigned char timeslice_task_isexist(TimesilceTaskObj* obj)
{
    unsigned char isexist = 0;
    ListObj* node;
    TimesilceTaskObj* task;

    list_for_each(node, &timeslice_task_list)
    {
        task = list_entry(node, TimesilceTaskObj, timeslice_task_list);
        if (obj->id == task->id)
            isexist = 1;
    }

    return isexist;
}

unsigned int timeslice_get_task_timeslice_len(TimesilceTaskObj* obj)
{
    return obj->timeslice_len;
}

底层侵入式双向链表

该链表是linux内核中使用十分广泛,也十分经典,其原理具体可以参考文章:
https://www.cnblogs.com/skywang12345/p/3562146.html

h文件:

#ifndef _LIST_H
#define _LIST_H

#define offset_of(type, member)             (unsigned long) &((type*)0)->member
#define container_of(ptr, type, member)     ((type *)((char *)(ptr) - offset_of(type, member)))

typedef struct list_structure
{
    struct list_structure* next;
    struct list_structure* prev;
} ListObj;

#define LIST_HEAD_INIT(name)    {&(name), &(name)}
#define LIST_HEAD(name)         ListObj name = LIST_HEAD_INIT(name)

void list_init(ListObj* list);
void list_insert_after(ListObj* list, ListObj* node);
void list_insert_before(ListObj* list, ListObj* node);
void list_remove(ListObj* node);
int list_isempty(const ListObj* list);
unsigned int list_len(const ListObj* list);

#define list_entry(node, type, member) \
    container_of(node, type, member)

#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)

#define list_for_each_safe(pos, n, head) \
  for (pos = (head)->next, n = pos->next; pos != (head); \
    pos = n, n = pos->next)

#endif

c文件:

#include "list.h"

void list_init(ListObj* list)
{
    list->next = list->prev = list;
}

void list_insert_after(ListObj* list, ListObj* node)
{
    list->next->prev = node;
    node->next = list->next;

    list->next = node;
    node->prev = list;
}

void list_insert_before(ListObj* list, ListObj* node)
{
    list->prev->next = node;
    node->prev = list->prev;

    list->prev = node;
    node->next = list;
}

void list_remove(ListObj* node)
{
    node->next->prev = node->prev;
    node->prev->next = node->next;

    node->next = node->prev = node;
}

int list_isempty(const ListObj* list)
{
    return list->next == list;
}

unsigned int list_len(const ListObj* list)
{
    unsigned int len = 0;
    const ListObj* p = list;
    while (p->next != list)
    {
        p = p->next;
        len++;
    }

    return len;
}

到此,一个全新的,完全解耦的,十分方便易用时间片轮询框架完成。

来源:ERYUESANHI
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 13

在单片机开发中,UART、I2C、RS485等普遍在用,对它们的认识可能模棱两可,本文把它们整理了一下。本文较长,阅读时间大约10分钟。

UART通用异步收发器

UART口指的是一种物理接口形式(硬件)。

“单片机常用的几种通信接口"

UART是异步,全双工串口总线。它比同步串口复杂很多。有两根线,一根TXD用于发送,一根RXD用于接收。

UART的串行数据传输不需要使用时钟信号来同步传输,而是依赖于发送设备和接收设备之间预定义的配置。

对于发送设备和接收设备来说,两者的串行通信配置应该设置为完全相同。

“单片机常用的几种通信接口"

起始位:表示数据传输的开始,电平逻辑为“0” 。

数据位:可能值有5、6、7、8、9,表示传输这几个bit 位数据。一般取值为8,因为一个ASCII 字符值为8 位。

奇偶校验位:用于接收方对接收到的数据进行校验,校验“1” 的位数为偶数(偶校验) 或奇数(奇校验),以此来校验数据传送的正确性,使用时不需要此位也可以。

停止位:表示一帧数据的结束。电平逻辑为“1”。

如果用通用IO口模拟UART总线,则需一个输入口,一个输出口。

I2C总线

I2C总线是一种同步、半双工双向的两线式串口总线。它由两条总线组成:串行时钟线SCL和串行数据线SDA。

SCL线——负责产生同步时钟脉冲。

SDA线——负责在设备间传输串行数据。

该总线可以将多个I2C设备连接到该系统上。连接到I2C总线上的设备既可以用作主设备,也可以用作从设备。

“单片机常用的几种通信接口"

主设备负责控制通信,通过对数据传输进行初始化,来发送数据并产生所需的同步时钟脉冲。从设备则是等待来自主设备的命令,并响应命令接收。

主设备和从设备都可以作为发送设备或接收设备。无论主设备是作为发送设备还是接收设备,同步时钟信号都只能由主设备产生。

如果用通用IO口模拟I2C总线,并实现双向传输,则需一个输入输出口(SDA),另外还需一个输出口(SCL)。

SPI串行外设接口

SPI总线是同步、全双工双向的4线式串行接口总线。它是由“单个主设备+多个从设备”构成的系统。

在系统中,只要任意时刻只有一个主设备是处于激活状态的,就可以存在多个SPI主设备。常运用于AD转换器、EEPROM、FLASH、实时时钟、数字信号处理器和数字信号解码器之间实现通信。

“单片机常用的几种通信接口"

为了实现通信,SPI共有4条信号线,分别是:

  • 主设备出、从设备入(Master Out Slave In,MOSI):由主设备向从设备传输数据的信号线,也称为从设备输入(Slave Input/Slave Data In,SI/SDI)。

  • 主设备入、从设备出(Master In Slave Out,MISO):由从设备向主设备传输数据的信号线,也称为从设备输出(Slave Output/Slave Data Out,SO/SDO)。

  • 串行时钟(Serial Clock,SCLK):传输时钟信号的信号线。

  • 从设备选择(Slave Select,SS):用于选择从设备的信号线,低电平有效。

SPI 的工作时序模式由CPOL(Clock Polarity,时钟极性)和CPHA(Clock Phase,时钟相位)之间的相位关系决定,CPOL 表示时钟信号的初始电平的状态,CPOL 为0 表示时钟信号初始状态为低电平,为1 表示时钟信号的初始电平是高电平。CPHA 表示在哪个时钟沿采样数据,CPHA 为0 表示在首个时钟变化沿采样数据,而CPHA 为1 则表示在第二个时钟变化沿采样数据。

“单片机常用的几种通信接口"

UART、SPI、I2C比较

  • I2C线更少,比UART、SPI更为强大,但是技术上也更加麻烦些,因为I2C需要有双向IO的支持,而且使用上拉电阻,抗干扰能力较弱,一般用于同一板卡上芯片之间的通信,较少用于远距离通信。

  • SPI实现要简单一些,UART需要固定的波特率,就是说两位数据的间隔要相等,而SPI则无所谓,因为它是有时钟的协议。

  • I2C的速度比SPI慢一点,协议比SPI复杂一点,但是连线也比标准的SPI要少。

  • UART一帧可以传5/6/7/8位,I2C必须是8位。I2C和SPI都从最高位开始传。

  • SPI用片选信号选择从机,I2C用地址选择从机。

“单片机常用的几种通信接口"

RS232串口通信

传输线有两根,地线一根。电平是负逻辑:

-3V~-15V逻辑“1”,+3V~+15V逻辑“0”。

RS-232串口通信传输距离15米左右。可做到双向传输,全双工通讯,传输速率低20kbps 。

下图是DB9公头和母头的定义,一般用的最多的是RXD、TXD、GND三个信号。

“单片机常用的几种通信接口"

TTL和RS-232互转

单片机接口一般是TTL电平,如果接232电平的外设,就需要加TTL转RS232的模块。如下图,可用芯片MAX232进行转换。

“单片机常用的几种通信接口"

RS422串口通信

RS-422有4根信号线:两根发送、两根接收和一根地线,是全双工通信。

它有一个主设备,其余为从设备,从设备之间不能通信,所以RS-422支持点对多的双向通信。

“单片机常用的几种通信接口"

RS485串口通信

RS-485采用平衡发送和差分接收,因此具有抑制共模干扰的能力。

采用两线半双工传输,最大速率10Mb/s,电平逻辑是两线的电平差来决定的,提高抗干扰能力,传输距离长(几十米到上千米)。

+2V~+6V逻辑“1”,-2~-6V逻辑“0”。

TTL转成RS-485很常见,比如MAX485,参考电路如下

“单片机常用的几种通信接口"

RE引脚:接收器输出使能(低电平有效)。

DE引脚:发送器输出使能(高电平有效)。可以直接通过MCU的IO端口控制。

TTL

嵌入式里面说的串口,一般是指UART口。4个pin(Vcc,GND,RX,TX),用TTL电平。

PC中的COM口即串行通讯端口,简称串口。9个Pin,用RS232电平。

“单片机常用的几种通信接口"

串口、COM口是指的物理接口形式(硬件)。而TTL、RS-232、RS-485是指电平标准(电信号)。

“单片机常用的几种通信接口"

单片机与PC通讯示意图如下:

“单片机常用的几种通信接口"

CAN总线

CAN是控制器局域网络的简称,是一种能够实现分布式实时控制的串行通信网络。CAN总线的功能复杂且智能。主要用于汽车通信。

CAN总线网络主要挂在CAN_H和CAN_L,各个节点通过这两条线实现信号的串行差分传输,为了避免信号的反射和干扰,还需要在CAN_H和CAN_L之间接上120欧姆的终端电阻。

“单片机常用的几种通信接口"

每一个设备既可做主设备也可做从设备。CAN总线的通信距离可达10千米(速率低于5Kbps),速度可达1Mbps(通信距离小于40M)。

“单片机常用的几种通信接口"

CAN电平逻辑

CAN总线采用"线与"的规则进行总线冲裁,1&0为0,所以称0为显性,1为隐性。

从电位上看,因为规定高电位为0,低电位为1,同时发出信号时实际呈现为高电位,从现象上看就像0覆盖了1,所以称0为显性,1为隐性。

“单片机常用的几种通信接口"

USB通信串行总线

USB接口最少有四根线,其中有两根是数据线,而所有的USB数据传输都是通过这两根线完成。它的通信远比串口复杂的多。

两根数据线采用差分传输,即需要两根数据线配合才能传输一个bit,因此是半双工通信,同一时间只能发送或者接收。

USB 规定,如果电压电平不变,代表逻辑1;如果电压电平变化,则代表逻辑0。

“单片机常用的几种通信接口"

USB转TTL

一般USB转串口都是用CH340G芯片。

“单片机常用的几种通信接口"

用串口通信比USB简单,因为串口通信没有协议。

SD卡

SD卡是一种存储卡,可用于手机作为内存卡使用。

嵌入式中,单片机与SD卡通信有两种模式:

  • SPI总线通信模式

  • SD总线通信模式

“单片机常用的几种通信接口"

值得注意的是,SD总线模式中有4条数据线;SPI总线模式中仅有一条数据线(MOSI和MISO不能同时读数据,也不能同时写数据);这样在嵌入式中,单片机与SD卡通信时采用SD总线模式比SPI总线模式速度快几倍。

“单片机常用的几种通信接口"

1-WIRE总线

1-Wire由美国Dallas(达拉斯)公司推出,是一种异步半双工串行传输。采用单根信号线,既传输时钟又传输数据,而且数据传输是双向的。

“单片机常用的几种通信接口"

单总线的数据传输速率一般为16.3Kbit/s,最大可达142 Kbit/s,通常情况下采用100Kbit/s以下的速率传输数据。

1-Wire线端口为漏极开路或三态门的端口,因此一般需要加上拉电阻Rp,通常选用5K~10KΩ

主要应用在:打印墨盒或医疗消耗品的识别;印刷电路板、配件及外设的识别和认证。

DMA直接存储器访问

DMA是STM32内的一个硬件模块,它独立于CPU,在外围设备和内存之间进行数据传输,解放了CPU,可使CPU的效率大大提高。

“单片机常用的几种通信接口"

它可以高速访问外设、内存,传输不受CPU的控制,并且是双向通信。因此,使用DMA可以大大提高数据传输速度,这也是ARM架构的一个亮点——DMA总线控制。

DMA就相应于一条高速公路,专用、高速的特性。如果不使用DMA,也可以达到目的,只是达到目的的时间比较长。

Ethernet以太网

以太网是目前应用最普遍的局域网技术。

大家知道,以太网接口可分为协议层和物理层。

协议层是由一个叫MAC(Media Access Layer)控制器的单一模块实现。

物理层由两部分组成,即PHY(Physical Layer)和传输器。

目前很多主板的南桥芯片已包含了以太网MAC控制功能,只是未提供物理层接口。因此,需外接PHY芯片以提供以太网的接入通道。

“单片机常用的几种通信接口"

网络变压器的作用是:

  • 耦合差分信号,抗干扰能力更强

  • 变压器隔离网线端不同设备的不同电平,隔离直流信号

以太网接口参考电路,如下图所示。

“单片机常用的几种通信接口"

来源:网络
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 49

在单片机编程中,有很多人会因为一些貌似简单的处理而把问题弄得乱七八糟,如林中蛛网一样,错综复杂。

而事实上,根据编程魔法之思想,对程序处理的过程严格划分部门、各施其职、部门内部互不干涉内政,是成功编程的关键。

也许我这样说,很多人还觉得很抽象。因为人人都知道模块化设计的理念,但是又有几人能把这个理念运用自如?

好,为了说明这个问题,我们举一例而示三。

现在,我们要编写一个单片机的数据显示程序。

根据单片机编程魔法师的面向对象思想,显然我们要把我们的显示处理进行独立化处理,这种处理的结果是:这个显示处理我们将得到一个显示器对象,这个对象就是一个独立的模块,当我们在对这个显示器对象进行使用的时候,我们不必感觉到这个显示器对象所对应的硬件是什么显示器,例如到底是液晶显示器还是8段数码管什么的。

我们都知道,我们在编程的时候拥有至高无上的ducai权力。但是如果你真的要行使这样的权力,那你和你的程序最终都将会痛不欲生,特别是当你的程序规模不断扩大之后。

对于类似诸如显示器这类的编程,我们首先得从思想上将其理清关系,要做到分块清晰,结构合理。

为了做到这一点,我们就对这种程序使用三权分立。如图:

“单片机编程魔法

这幅图,配上三权分立思想,相信大家都能明白吧?这里就不多解释了。很多人会想:这思想想想就能想到。

本例子不考虑图像与动画处理,也不考虑单屏显示不下的问题。

首先,我们考虑三权分立中的数据区的管辖权。

数据区存放显示用的所有数据,我们以字符型显示器为例,数据区保存所有要显示的。

言下之意,其它地方不能有显示所需要的数据。

我们称这个数据区为显存。相信看到这个词,大家多少能想起点什么吧。

下面我们就简单地对显存进行一个定义:

#define ROWS        2
#define COLS          16
unsigned char vm[ROWS][COLS];

显存定义便结束了。

其显存者,分立三权之其一也。

本来,我们可以直接修改显存以更改显示内容,但是考虑到大通用与大继承,所以我们不能那么做。

因此我们不允许直接读写显存,为此,我们得提供一个通用的读写工具,如下:

// 功能:写显存
// 参数:r - 要写入的行
//           c - 要写入的列
//           s - 要写入的字符串
void WriteVM(unsigned char r, unsigned char c, unsigned char *s)
{
// 此处调用显示定位函数(本帖不讨论此函数)
// 此处处理显示字符串
}

这样一来,我们就有了控制操作显存之大发,接下来,我们就要考虑如何处理显存内容的显示了。

此等大发,诸位魔法师何不先撞头以修炼之?

接下来,显示显存的内容,便成了显示处理的关键。

显示显存的内容,无非就两个情况:一是需要不断更新的情况,二是需要即时更新的情况。

如果需要不断更新或有部分内容需要不断更新,这问题就好处理了。只需要提供一个不断刷新显示的函数就可以了,例子如下:

void showVM(void)
{
// 将显存的全部内容即时送显示器,即整屏刷新
// 部分不需要不断刷新的数据均使用不断更新的思想进行刷新
// 这种方法不适用单片机处理能力过差的情况
}

当然,如果有的魔法师不希望使用那种整屏刷新的办法,则只需要修改前面的WriteVM()函数为边写显存边刷新显示的办法即可。但是这种办法缺乏灵活性,我不建议这样做。因为现在的单片机一般都有足够的能力来处理显示这点事。

当然,写好一个showVM( )并不容易,因为有的显示屏可能会点阵很多。

这个时候,我们就得采用单行扫描法,以降低showVM( )对单片机ALU的占有率。单行扫描法即每次调用只刷新显示器的某一行或某一个部分。这就是《单片机编程魔法师》中的线程处理办法的一个具体的应用。

而当showVM( )写完后,显示器这个原本复杂的对象,也就被我们大大简化了。这简单的两个函数,即分立三权之其二也。

既然为三权分立,以下来说其三。

因为有了其一、其二的思想基础,其三便只是一个极为简单的运用了。我们可以毫无担忧的随处向显存写入要显示的内容,而不必担心它们如何显示、如何刷新。

这显然是一个大好消息。现在我们只需要把这个好消息写在纸上。例如:

void main( )
 {
   while(1)
   {
     ……
    WriteVM(x,y,"");   //可以在任何一个位置随意显示内容,而不必考虑任何显示问题,只需要考虑如何填入参数即可
    ……
    showVM( );        //此处只需一个简单的调用,不必在使用是考虑其它任何问题
  }
}

画此思想的空间框图如下:

“嵌入式单片机编程魔法之三权分立"

最后,再次对此思想的运用做个总结。

在我提出裸编程面向对象思想之前,很多人都使用过编程语言所提供的面向对象编程。我也一样,之前使用了很多年。

既然大家都是用过面向对象编程的,这个起点大家都一样,也不值得一提,所以我几乎不说那时候的事情。

既然我提出面向对象的裸化,那就是一定与过去有所不同,否则我就是在这里哗众取宠、吃别人嚼过的馍了,而且这种替他人阿道式的宣传也绝无意义,随便到书店走一趟,相关书籍一大堆。

裸机编程总结

我再次指明:裸编程中的一切思想都是取自于过去的思想、但是又不同于过去的思想,其实现手法与传统的思想并不相同。裸编程思想忽略了语法的约束,忽略了工具的支持,将传统的思想进行了极大的简化,未引入任何额外的知识,从而让过去只有在足够的硬件、软件支持的方法,能够在无需任何额外软件支持以及只需极其简单的硬件中得以有效的使用。

这种思想与传统思想是一脉而不相同,同科而不同类。

很多人看了书,会认识那些概念都似曾相识,但是似曾相识,不等于获得真理。有没有获得真理,要看你能不能施出魔法。

正如C语言一样,它只用少的符号来描述世界,与人类语言大不相同,如英语、汉语。描述的符号越少,越是难以描述世界。因为符号少,可用的语素也就少。语素少,语法好学,但是用少量的语素去描述无穷的世界,会造成描述方法的复杂。

来源:技术让梦想更伟大(整理:李肖遥)
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 12

1、单片机中如果没有了晶振会怎么样?

首先我们要知道单片机到底是什么?简单来说,它就是一个微型计算机系统。然而麻雀虽小,五脏俱全。

单片机内部用到很多和电脑功能相类似的模块,像CPU、内存、并行总线、存储数据的存储器等在单片机中都存在,不过不同的是它的这些部件性能相比电脑要弱很多,当然价钱也相对要低不少。我们可以用它来做一些控制电器等不是很复杂的工作,它主要是作为电子产品控制部分的核心部件。

那单片机晶振又是什么呢?单片机中若是没有了晶振会怎么样呢?

单片机晶振就是单片机内部电路产生单片机所需的时钟频率的电子元件,单片机晶振提供的时钟频率越高,那么单片机运行速度就越快,单片机接的一切指令的执行都是建立在其晶振提供的时钟频率,由此可见单片机中晶振的重要性了。

通常一个单片机系统共用一个晶振,便于各部分保持同步。有些通讯系统的基频和射频使用不同的晶振,我们可以通过电子调整频率的方法保持同步。

单片机系统中晶振的主要作用就是为系统提供基本的时钟信号,晶振通常与锁相环电路配合使用,来提供系统所需的时钟频率。如果不同子系统需要不同频率的时钟信号,可以用与同一个晶振相连的不同锁相环来提供。

“没有了晶振的单片机会怎样?

所以说,单片机中没有了晶振,也就没有时钟周期,没有时钟周期,就无法执行程序代码,单片机就无法工作,程序也就无法烧入。因为单片机工作时,是一条一条地从RoM中取指令,然后逐步执行。

我们把单片机访问一次存储器的时间称之为一个机器周期,这是一个时间基准。—个机器周期包括12个时钟周期。如果一个单片机选择了12MHZ晶振,它的时钟周期是1/12us,它的一个机器周期是12×(1/12)us,也就是1us。

机器周期不仅对于指令执行有着重要的意义,而且机器周期也是单片机定时器和计数器的时间基准。若一个单片机选择了12MHZ的晶振(这个晶振可以是49S的插件晶振,也可以是贴片晶振),那么当定时器的数值加1时,实际经过的时间就是1us,这就是单片机的定时原理。

我们的电脑用过一段时间后总是会出这样那样的问题,可能是主板、CPU、电源烧坏了等严重的情况,当然也有可能仅仅是内存条有了松动、主板上的一个晶振损坏了甚至是长期没有清理灰尘等小问题,大问题我们没有办法,只能送去维修了,但是这些小问题相信我们都能够轻松解决的。

单片机也是一样,若是单片机无法启动,不要认为它已经坏了,就马上将它扔掉了,很多情况下单片机无法工作都是其中的石英晶振出现了问题。这时我们可以用简单的方法来测量晶振是否损坏。

方法很简单,我们用万用表测量晶振两个引脚电压是否是芯片工作电压的一半,比如51单片机的工作电压是+5V,则我们测量是否是2.5V左右。另外如果用镊子碰晶体另外一个脚,若是这个电压有明显变化,证明晶振是起振的。反之,则是晶振已经损坏了,我们只需更换晶振就可再次使用单片机了。

以上着重讲了石英晶振在单片机中的重要性,然而,作为一种精密的频率元件,单片机中的晶振却很容易出现问题,轻微的碰撞都可能导致晶振损坏,因此,遇到单片机晶振不起振是很常见的一种现象。那么,单片机晶振经常遇到的问题及处理方法有哪些?

2、晶振不起振的原因分析

首先,我们分析引起单片机晶振不起振的原因有哪些。

1、PCB布线错误,现在的PCB不再是单一功能电路(数字或模拟电路),而是由数字电路和模拟电路混合组成的。因此,PCB布线的时候可能出现问题导致晶振不起振;

2、单片机或晶振的质量问题;

3、负载二极管或匹配电容与晶振不匹配或者电容质量有问题;

4、PCB板受潮,导致阻抗失配而不能起振;

5、晶振电路的走线过长或两脚之间有走线导致晶振不起振,通常我们在PCB布线时晶振电路的走线应尽量短且尽可能靠近振荡器,严禁在晶振两脚间走线;

6、晶振受外围电路的影响而不起振。

“没有了晶振的单片机会怎样?"

3、其他要特别注意的问题分析

1)晶振的选型,选择合适的晶振对单片机来说非常重要,我们在选择晶振的时候至少必须考虑谐振频点、负载电容、激励功率、温度特性长期稳定性等参数。合适的晶振才能确保单片机能够正常工作。

2)电容引起的晶振不稳定,晶振电路中的电容C1和C2两个电容对晶振的稳定性有很大影响,每一种晶振都有各自的特性,所以我们必须按晶振生产商所提供的数值选择外部元器件。

通常在许可范围内,C1,C2值越低越好,C值偏大虽有利于振荡器的稳定,但将会增加起振时间。一般情况下我们使得C2值大于C1值,这样可使得上电时加快晶振起振。

3)单片机晶振被过分驱动引起的问题,晶振被过分驱动会渐渐损耗晶振的接触电镀从而引起晶振频率的上升。

我们可用一台示波器来检测,OSC,输出脚,如果检测一非常清晰的正弦波且正弦波的上限值和下限值都符合时钟输入需要,则晶振未被过分驱动,相反,如果正弦波形的波峰,波谷两端被削平,而使波形成为方形,则晶振被过分驱动,这时就需要用电阻RS来防止晶振被过分驱动,判断电阻RS值大小的最简单的方法就是串联一个5k或10k的微调电阻,从0开始慢慢调高,一直到正弦波不再被削平为止,通过此办法就可以找到最接近的电阻RS值。

4)画PCB的时候,要求晶振离它的放大电路(IC管脚)越近越好。这是由于晶振的输出能力有限,它仅仅输出以毫瓦为单位的电能量。在IC(集成电路)内部,通过放大器将这个信号放大几百倍甚至上千倍才能正常使用。

晶振和IC间一般是通过铜走线相连的,这根走线可以看成一段电容或数段导线,导线在切割磁力线的时候会产生电流,导线越长,产生的电流越强。

晶振好比是单片机的心脏!我们都知道,单片机晶振的作用是为系统提供基本的时钟信号。通常一个系统共用一个晶振,便于各部分保持同步。不同型号的单片机使用的石英晶振型号及频率也可能是不一样的。

单片机中的晶振若是出了问题,单片机也就无法正常工作了。因此,若是发现你的单片机无法正常工作,很大程度上可能是晶振问题造成的。

来源:EDA365电子论坛
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 14

本文将以STM32F10x为例,对标准库开发进行概览。主要分为三块内容:

  • STM32系统结构

  • 寄存器

  • 通过点灯案例,详解如何基于标准库构建STM32工程

STM32系统结构

“STM32f10xxx系统结构"
STM32f10xxx系统结构

内核IP

从结构框图上看,Cortex-M3内部有若干个总线接口,以使CM3能同时取址和访内(访问内存),它们是:指令存储区总线(两条)、系统总线、私有外设总线。有两条代码存储区总线负责对代码存储区(即 FLASH 外设)的访问,分别是 I-Code 总线和 D-Code 总线。

I-Code用于取指,D-Code用于查表等操作,它们按最佳执行速度进行优化。

系统总线(System)用于访问内存和外设,覆盖的区域包括SRAM,片上外设,片外RAM,片外扩展设备,以及系统级存储区的部分空间。

私有外设总线负责一部分私有外设的访问,主要就是访问调试组件。它们也在系统级存储区。

还有一个DMA总线,从字面上看,DMA是data memory access的意思,是一种连接内核和外设的桥梁,它可以访问外设、内存,传输不受CPU的控制,并且是双向通信。简而言之,这个家伙就是一个速度很快的且不受老大控制的数据搬运工。

处理器外设(内核之外的外设)

从结构框图上看,STM32的外设有串口、定时器、IO口、FSMC、SDIO、SPI、I2C等,这些外设按照速度的不同,分别挂载到AHB、APB2、APB1这三条总线上。

寄存器

什么是寄存器?寄存器是内置于各个IP外设中,是一种用于配置外设功能的存储器,并且有想对应的地址。一切库的封装始于映射。

“入手STM32单片机的知识点总结"

“入手STM32单片机的知识点总结"

是不是看的眼都花了,如果进行寄存器开发,就需要怼地址以及对寄存器进行字节赋值,不仅效率低而且容易出错。

库的存在就是为了解决这类问题,将代码语义化。语义化思想不仅仅是嵌入式有的,前端代码也在追求语义特性。

从点灯开始学习STM32

“入手STM32单片机的知识点总结"

内核库文件分析

cor_cm3.h

这个头文件实现了:

1、内核结构体寄存器定义。

2、内核寄存器内存映射。

3、内存寄存器位定义。跟处理器相关的头文件stm32f10x.h实现的功能一样,一个是针对内核的寄存器,一个是针对内核之外,即处理器的寄存器。

misc.h

内核应用函数库头文件,对应stm32f10x_xxx.h。

misc.c

内核应用函数库文件,对应stm32f10x_xxx.c。在CM3这个内核里面还有一些功能组件,如NVIC、SCB、ITM、MPU、CoreDebug,CM3带有非常丰富的功能组件,但是芯片厂商在设计MCU的时候有一些并不是非要不可的,是可裁剪的,比如MPU、ITM等在STM32里面就没有。

其中NVIC在每一个CM3内核的单片机中都会有,但都会被裁剪,只能是CM3 NVIC的一个子集。在NVIC里面还有一个SysTick,是一个系统定时器,可以提供时基,一般为操作系统定时器所用。misc.h和mics.c这两个文件提供了操作这些组件的函数,并可以在CM3内核单片机直接移植。

处理器外设库文件分析

startup_stm32f10x_hd.s

这个是由汇编编写的启动文件,是STM32上电启动的第一个程序,启动文件主要实现了

  • 初始化堆栈指针 SP;
  • 设置 PC 指针=Reset_Handler ;
  • 设置向量表的地址,并 初始化向量表,向量表里面放的是 STM32 所有中断函数的入口地址;
  • 调用库函数 SystemInit,把系统时钟配置成 72M,SystemInit 在库文件 stytem_stm32f10x.c 中定义;
  • 跳转到标号_main,最终去到 C 的世界。

system_stm32f10x.c

这个文件的作用是里面实现了各种常用的系统时钟设置函数,有72M,56M,48, 36,24,8M,我们使用的是是把系统时钟设置成72M。

Stm32f10x.h

这个头文件非常重要,这个头文件实现了:

1、处理器外设寄存器的结构体定义。

2、处理器外设的内存映射。

3、处理器外设寄存器的位定义。

关于 1 和 2 我们在用寄存器点亮 LED 的时候有讲解。

其中 3:处理器外设寄存器的位定义,这个非常重要,具体是什么意思?

我们知道一个寄存器有很多个位,每个位写 1 或者写 0 的功能都是不一样的,处理器外设寄存器的位定义就是把外设的每个寄存器的每一个位写 1 的 16 进制数定义成一个宏,宏名即用该位的名称表示,如果我们操作寄存器要开启某一个功能的话,就不用自己亲自去算这个值是多少,可以直接到这个头文件里面找。

我们以片上外设 ADC 为例,假设我们要启动 ADC 开始转换,根据手册我们知道是要控制 ADC_CR2 寄存器的位 0:ADON,即往位 0 写 1,即:

ADC->CR2=0x00000001;

这是一般的操作方法。现在这个头文件里面有关于 ADON 位的位定义:

 #define ADC_CR2_ADON ((uint32_t)0x00000001)

有了这个位定义,我们刚刚的代码就变成了:

ADC->CR2=ADC_CR2_ADON

stm32f10x_xxx.h

外设 xxx 应用函数库头文件,这里面主要定义了实现外设某一功能的结构体,比如通用定时器有很多功能,有定时功能,有输出比较功能,有输入捕捉功能,而通用定时器有非常多的寄存器要实现某一个功能。

比如定时功能,我们根本不知道具体要操作哪些寄存器,这个头文件就为我们打包好了要实现某一个功能的寄存器,是以机构体的形式定义的,比如通用定时器要实现一个定时的功能,我们只需要初始化 TIM_TimeBaseInitTypeDef 这个结构体里面的成员即可,里面的成员就是定时所需要操作的寄存器。

有了这个头文件,我们就知道要实现某个功能需要操作哪些寄存器,然后再回手册中精度这些寄存器的说明即可。

stm32f10x_xxx.c

stm32f10x_xxx.c:外设 xxx 应用函数库,这里面写好了操作 xxx 外设的所有常用的函数,我们使用库编程的时候,使用的最多的就是这里的函数。

SystemInit

工程中新建main.c 。

在此文件中编写main函数后直接编译会报错:

Undefined symbol SystemInit (referred from startup_stm32f10x_hd.o).

错误提示说SystemInit没有定义。从分析启动文件startup_stm32f10x_hd.s时我们知道,

;Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
;IMPORT SystemInit
;LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP

汇编中;分号是注释的意思

第五行第六行代码Reset_Handler调用了SystemInit该函数用来初始化系统时钟,而该函数是在库文件system_stm32f10x.c中实现的。我们重新写一个这样的函数也可以,把功能完整实现一遍,但是为了简单起见,我们在main文件里面定义一个SystemInit空函数,为的是骗过编译器,把这个错误去掉。

关于配置系统时钟之后会出文章RCC时钟树详细介绍,主要配置时钟控制寄存器(RCC_CR)和时钟配置寄存器(RCC_CFGR)这两个寄存器,但最好是直接使用CubeMX直接生成,因为它的配置过程有些冗长。

如果我们用的是库,那么有个库函数SystemInit,会帮我们把系统时钟设置成72M。

现在我们没有使用库,那现在时钟是多少?答案是8M,当外部HSE没有开启或者出现故障的时候,系统时钟由内部低速时钟LSI提供,现在我们是没有开启HSE,所以系统默认的时钟是LSI=8M。

库封装层级

“入手STM32单片机的知识点总结"

如图,达到第四层级便是我们所熟知的固件库或HAL库的效果。当然库的编写还需要考虑许多问题,不止于这些内容。我们需要的是了解库封装的大概过程。

将库封装等级分为四级来介绍是为了有层次感,就像打怪升级一样,进行认知理解的升级。

我们都知道,操作GPIO输出分三大步:

时钟控制:

STM32 外设很多,为了降低功耗,每个外设都对应着一个时钟,在系统复位的时候这些时钟都是被关闭的,如果想要外设工作,必须把相应的时钟打开。

STM32 的所有外设的时钟由一个专门的外设来管理,叫RCC(reset and clockcontrol),RCC 在STM32 参考手册的第六章。

STM32 的外设因为速率的不同,分别挂载到三条总系上:AHB、APB2、APB1,AHB为高速总线,APB2 次之,APB1 再次之。所以的IO 口都挂载到APB2 总线上,属于高速外设。

模式配置:

这个由端口配置寄存器来控制。端口配置寄存器分为高低两个,每4bit 控制一个IO 口,所以端口配置低寄存器:CRL 控制这IO 口的低8 位,端口配置高寄存器:CRH控制这IO 口的高8bit。

在4 位一组的控制位中,CNFy[1:0] 用来控制端口的输入输出,MODEy[1:0]用来控制输出模式的速率,又称驱动电路的响应速度,注意此处速率与程序无关,GPIO引脚速度、翻转速度、输出速度区别输入有4种模式,输出有4种模式,我们在控制LED 的时候选择通用推挽输出。

输出速率有三种模式:2M、10M、50M,这里我们选择2M。

电平控制:

STM32的IO口比较复杂,如果要输出1和0,则要通过控制:端口输出数据寄存器ODR来实现,ODR 是:Output data register的简写,在STM32里面,其寄存器的命名名称都是英文的简写,很容易记住。

从手册上我们知道ODR是一个32位的寄存器,低16位有效,高16位保留。低16位对应着IO0~IO16,只要往相应的位置写入0或者1就可以输出低或者高电平。

第一层级:基地址宏定义

“入手STM32单片机的知识点总结"

时钟控制:

“入手STM32单片机的知识点总结"

在STM32中,每个外设都有一个起始地址,叫做外设基地址,外设的寄存器就以这个基地址为标准按照顺序排列,且每个寄存器32位,(后面作为结构体里面的成员正好内存对齐)。

查表看到时钟由APB2外设时钟使能寄存器(RCC_APB2ENR)来控制,其中PB端口的时钟由该寄存器的位3写1使能。我们可以通过基地址+偏移量0x18,算出RCC_APB2ENR的地址为:0x40021018。那么使能PB口的时钟代码则如下所示:

 #define RCC_APB2ENR *(volatile unsigned long *)0x40021018

 // 开启端口B 时钟
 RCC_APB2ENR |= 1<<3;

模式配置:

“入手STM32单片机的知识点总结"

同RCC_APB2ENR一样,GPIOB的起始地址是:0X4001 0C00,我们也可以算出GPIO_CRL的地址为:0x40010C00。那么设置PB0为通用推挽输出,输出速率为2M的代码则如下所示:

“入手STM32单片机的知识点总结"

同上,从手册中我们看到ODR寄存器的地址偏移是:0CH,可以算出GPIOB_ODR寄存器的地址是:0X4001 0C00 + 0X0C = 0X4001 0C0C。现在我们就可以定义GPIOB_ODR这个寄存器了,代码如下:

#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C

//PB0 输出低电平
GPIOB_ODR = 0<<0;

第一层级:基地址宏定义完成用STM32控制一个LED的完整代码:

#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
#define GPIOB_CRL *(volatile unsigned long *)0x40010C00
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C

int main(void)
{
 // 开启端口B 的时钟
 RCC_APB2ENR |= 1<<3;

 // 配置PB0 为通用推挽输出模式,速率为2M
 GPIOB_CRL = (2<<0) | (0<<2);

 // PB0 输出低电平,点亮LED
 GPIOB_ODR = 0<<0;
}

void SystemInit(void)
{
}

第二层级:基地址宏定义+结构体封装

外设寄存器结构体封装

上面我们在操作寄存器的时候,操作的是寄存器的绝对地址,如果每个寄存器都这样操作,那将非常麻烦。我们考虑到外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占32个或者16个字节,这种方式跟结构体里面的成员类似。

所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。

下面我们先定义一个GPIO寄存器结构体,结构体里面的成员是GPIO的寄存器,成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。

typedef struct 
{
 volatile uint32_t CRL;
 volatile uint32_t CRH;
 volatile uint32_t IDR;
 volatile uint32_t ODR;
 volatile uint32_t BSRR;
 volatile uint32_t BRR;
 volatile uint32_t LCKR;
} GPIO_TypeDef;

在《STM32 中文参考手册》8.2 寄存器描述章节,我们可以找到结构体里面的7个寄存器描述。在点亮LED的时候我们只用了CRL和ODR这两个寄存器,至于其他寄存器的功能大家可以自行看手册了解。

在GPIO结构体里面我们用了两个数据类型,一个是uint32_t,表示无符号的32位整型,因为GPIO的寄存器都是32位的。这个类型声明在标准头文件stdint.h 里面使用typedef对unsigned int重命名,我们在程序上只要包含这个头文件即可。

另外一个是volatile作用就是告诉编译器这里的变量会变化不因优化而省略此指令,必须每次都直接读写其值,这样就能确保每次读或者写寄存器都真正执行到位。

STM32F1系列的GPIO端口分A~G,即GPIOA、GPIOB。GPIOG。每个端口都含有GPIO_TypeDef结构体里面的寄存器,我们可以根据手册各个端口的基地址把GPIO的各个端口定义成一个GPIO_TypeDef类型指针,然后我们就可以根据端口名(实际上现在是结构体指针了)来操作各个端口的寄存器,代码实现如下:

#define GPIOA ((GPIO_TypeDef *) 0X4001 0800)
#define GPIOB ((GPIO_TypeDef *) 0X4001 0C00)
#define GPIOC ((GPIO_TypeDef *) 0X4001 1000)
#define GPIOD ((GPIO_TypeDef *) 0X4001 1400)
#define GPIOE ((GPIO_TypeDef *) 0X4001 1800)
#define GPIOF ((GPIO_TypeDef *) 0X4001 1C00)
#define GPIOG ((GPIO_TypeDef *) 0X4001 2000)

外设内存映射

讲到基地址的时候我们再引人一个知识点:Cortex-M3存储器系统,这个知识点在《Cortex-M3权威指南》第5章里面讲到。CM3的地址空间是4GB,如下图所示:

“入手STM32单片机的知识点总结"

我们这里要讲的是片上外设,就是我们所说的寄存器的根据地,其大小总共有512MB,512MB是其极限空间,并不是每个单片机都用得完,实际上各个MCU厂商都只是用了一部分而已。STM32F1系列用到了:0x4000 0000 ~0x5003 FFFF。现在我们说的STM32的寄存器就是位于这个区域

APB1、APB2、AHB 总线基地址

现在我们说的STM32的寄存器就是位于这个区域,这里面ST设计了三条总线:AHB、APB2和APB1,其中AHB和APB2是高速总线,APB1是低速总线。不同的外设根据速度不同分别挂载到这三条总线上。

从下往上依次是:APB1、APB2、AHB,每个总线对应的地址分别是:APB1:0x40000000,APB2:0x4001 0000,AHB:0x4001 8000。

这三条总线的基地址我们是从《STM32 中文参考手册》2.3小节—存储器映像得到的:APB1的基地址是TIM2定时器的起始地址,APB2的基地址是AFIO的起始地址,AHB的基地址是SDIO的起始地址。其中APB1地址又叫做外设基地址,是所有外设的基地址,叫做PERIPH_BASE。

现在我们把这三条总线地址用宏定义出来,以后我们在定义其他外设基地址的时候,只需要在这三条总线的基址上加上偏移地址即可,代码如下:

#define PERIPH_BASE ((uint32_t)0x40000000)
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)

GPIO 端口基地址

因为GPIO挂载到APB2总线上,那么现在我们就可以根据APB2的基址算出各个GPIO端口的基地址,用宏定义实现代码如下:

#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)

第二层级:基地址宏定义+结构体封装完成用STM32控制一个LED的完整代码:

#include <stdint.h>
#define __IO volatile

typedef struct 
{
 __IO uint32_t CRL;
 __IO uint32_t CRH;
 __IO uint32_t IDR;
 __IO uint32_t ODR;
 __IO uint32_t BSRR;
 __IO uint32_t BRR;
 __IO uint32_t LCKR;
} GPIO_TypeDef;

typedef struct 
{
 __IO uint32_t CR;
 __IO uint32_t CFGR;
 __IO uint32_t CIR;
 __IO uint32_t APB2RSTR;
 __IO uint32_t APB1RSTR;
 __IO uint32_t AHBENR;
 __IO uint32_t APB2ENR;
 __IO uint32_t APB1ENR;
 __IO uint32_t BDCR;
 __IO uint32_t CSR;
} RCC_TypeDef;

#define PERIPH_BASE ((uint32_t)0x40000000)

#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)

#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)

#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define RCC ((RCC_TypeDef *) RCC_BASE)


#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
#define GPIOB_CRL *(volatile unsigned long *)0x40010C00
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C

int main(void)
{
 // 开启端口B 的时钟
 RCC->APB2ENR |= 1<<3;

 // 配置PB0 为通用推挽输出模式,速率为2M
 GPIOB->CRL = (2<<0) | (0<<2);

 // PB0 输出低电平,点亮LED
 GPIOB->ODR = 0<<0;
}

void SystemInit(void)
{
}

第二层级变化:

①、定义一个外设(GPIO)寄存器结构体,结构体的成员包含该外设的所有寄存器,成员的排列顺序跟寄存器偏移地址一样,成员的数据类型跟寄存器的一样。

②外设内存映射,即把地址跟外设建立起一一对应的关系。

③外设声明,即把外设的名字定义成一个外设寄存器结构体类型的指针。

④通过结构体操作寄存器,实现点亮LED。

第三层级:基地址宏定义+结构体封装+“位封装”(每一位的对应字节封装)

上面我们在控制GPIO输出内容的时候控制的是ODR(Output data register)寄存器,ODR是一个16位的寄存器,必须以字的形式控制其实我们还可以控制BSRR和BRR这两个寄存器来控制IO的电平,下面我们简单介绍下BRR寄存器的功能,BSRR自行看手册研究。

“入手STM32单片机的知识点总结"

位清除寄存器BRR只能实现位清0操作,是一个32位寄存器,低16位有效,写0没影响,写1清0。现在我们要使PB0输出低电平,点亮LED,则只要往BRR的BR0位写1即可,其他位为0,代码如下:

GPIOB->BRR = 0X0001;

这时PB0就输出了低电平,LED就被点亮了。

如果要PB2输出低电平,则是:

GPIOB->BRR = 0X0004;

如果要PB3/4/5/6。这些IO输出低电平呢?

道理是一样的,只要往BRR的相应位置赋不同的值即可。因为BRR是一个16位的寄存器,位数比较多,赋值的时候容易出错,而且从赋值的16进制数字我们很难清楚的知道控制的是哪个IO。

这时,我们是否可以把BRR的每个位置1都用宏定义来实现,如GPIO_Pin_0就表示0X0001,GPIO_Pin_2就表示0X0004。只要我们定义一次,以后都可以使用,而且还见名知意。“位封装”(每一位的对应字节封装) 代码如下:

#define GPIO_Pin_0 ((uint16_t)0x0001) /*!< Pin 0 selected */
#define GPIO_Pin_1 ((uint16_t)0x0002) /*!< Pin 1 selected */
#define GPIO_Pin_2 ((uint16_t)0x0004) /*!< Pin 2 selected */
#define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */
#define GPIO_Pin_4 ((uint16_t)0x0010) /*!< Pin 4 selected */
#define GPIO_Pin_5 ((uint16_t)0x0020) /*!< Pin 5 selected */
#define GPIO_Pin_6 ((uint16_t)0x0040) /*!< Pin 6 selected */
#define GPIO_Pin_7 ((uint16_t)0x0080) /*!< Pin 7 selected */
#define GPIO_Pin_8 ((uint16_t)0x0100) /*!< Pin 8 selected */
#define GPIO_Pin_9 ((uint16_t)0x0200) /*!< Pin 9 selected */
#define GPIO_Pin_10 ((uint16_t)0x0400) /*!< Pin 10 selected */
#define GPIO_Pin_11 ((uint16_t)0x0800) /*!< Pin 11 selected */
#define GPIO_Pin_12 ((uint16_t)0x1000) /*!< Pin 12 selected */
#define GPIO_Pin_13 ((uint16_t)0x2000) /*!< Pin 13 selected */
#define GPIO_Pin_14 ((uint16_t)0x4000) /*!< Pin 14 selected */
#define GPIO_Pin_15 ((uint16_t)0x8000) /*!< Pin 15 selected */
#define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< All pins selected */

这时PB0就输出了低电平的代码就变成了:

GPIOB->BRR = GPIO_Pin_0;

(如果同时让PB0/PB15输出低电平,用或运算,代码:

GPIOB->BRR = GPIO_Pin_0|GPIO_Pin_15;

为了不使main函数看起来冗余,上述库封装 的代码不应该放在main里面,因为其是跟GPIO相关的,我们可以把这些宏放在一个单独的头文件里面。

在工程目录下新建stm32f10x_gpio.h,把封装代码放里面,然后把这个文件添加到工程里面。这时我们只需要在main.c里面包含这个头文件即可。

第四层级:基地址宏定义+结构体封装+“位封装”+函数封装

我们点亮LED的时候,控制的是PB0这个IO,如果LED接到的是其他IO,我们就需要把GPIOB修改成其他的端口,其实这样修改起来也很快很方便。

但是为了提高程序的可读性和可移植性,我们是否可以编写一个专门的函数用来复位GPIO的某个位,这个函数有两个形参,一个是GPIOX(X=A...G),另外一个是GPIO_Pin(0...15),函数的主体则是根据形参GPIOX 和GPIO_Pin来控制BRR寄存器,代码如下:

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
 GPIOx->BRR = GPIO_Pin;
}

这时,PB0输出低电平,点亮LED的代码就变成了:

GPIO_ResetBits(GPIOB,GPIO_Pin_0);

同理, 我们可以控制BSRR这个寄存器来实现关闭LED,代码如下:

// GPIO 端口置位函数
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
 GPIOx->BSRR = GPIO_Pin;
}

这时,PB0输出高电平,关闭LED的代码就变成了:

GPIO_SetBits(GPIOB,GPIO_Pin_0);

同样,因为这个函数是控制GPIO的函数,我们可以新建一个专门的文件来放跟gpio有关的函数。

在工程目录下新建stm32f10x_gpio.c,把GPIO相关的函数放里面。这时我们是否发现刚刚新建了一个头文件stm32f10x_gpio.h,这两个文件存放的都是跟外设GPIO相关的。

C文件里面的函数会用到h头文件里面的定义,这两个文件是相辅相成的,故我们在stm32f10x_gpio.c 文件中也包含stm32f10x_gpio.h这个头文件。别忘了把stm32f10x.h这个头文件也包含进去,因为有关寄存器的所有定义都在这个头文件里面。

如果我们写其他外设的函数,我们也应该跟GPIO一样,新建两个文件专门来存函数,比如RCC这个外设我们可以新建stm32f10x_rcc.c和stm32f10x_rcc.h。其他外依葫芦画瓢即可。

实例编写

以上,是对库封住过程的概述,下面我们正在地使用库函数编写LED程序

①管理库的头文件

当我们开始调用库函数写代码的时候,有些库我们不需要,在编译的时候可以不编译,可以通过一个总的头文件stm32f10x_conf.h来控制,该头文件主要代码如下:

//#include "stm32f10x_adc.h"
//#include "stm32f10x_bkp.h"
//#include "stm32f10x_can.h"
//#include "stm32f10x_cec.h"
//#include "stm32f10x_crc.h"
//#include "stm32f10x_dac.h"
//#include "stm32f10x_dbgmcu.h"
//#include "stm32f10x_dma.h"
//#include "stm32f10x_exti.h"
//#include "stm32f10x_flash.h"
//#include "stm32f10x_fsmc.h"
#include "stm32f10x_gpio.h"
//#include "stm32f10x_i2c.h"
//#include "stm32f10x_iwdg.h"
//#include "stm32f10x_pwr.h"
#include "stm32f10x_rcc.h"
//#include "stm32f10x_rtc.h"
//#include "stm32f10x_sdio.h"
//#include "stm32f10x_spi.h"
//#include "stm32f10x_tim.h"
//#include "stm32f10x_usart.h"
//#include "stm32f10x_wwdg.h"
//#include "misc.h"

这里面包含了全部外设的头文件,点亮一个LED我们只需要RCC和GPIO 这两个外设的库函数即可,其中RCC控制的是时钟,GPIO控制的具体的IO口。所以其他外设库函数的头文件我们注释掉,当我们需要的时候就把相应头文件的注释去掉即可。

stm32f10x_conf.h这个头文件在stm32f10x.h这个头文件的最后面被包含,在第8296行:

#ifdef USE_STDPERIPH_DRIVER
#include "stm32f10x_conf.h"
#endif

代码的意思是,如果定义了USE_STDPERIPH_DRIVER这个宏的话,就包含stm32f10x_conf.h这个头文件。

我们在新建工程的时候,在魔术棒选项卡C/C++中,我们定义了USE_STDPERIPH_DRIVER 这个宏,所以stm32f10x_conf.h 这个头文件就被stm32f10x.h包含了,我们在写程序的时候只需要调用一个头文件:stm32f10x.h即可。

②编写LED初始化函数

经过寄存器点亮LED的操作,我们知道操作一个GPIO输出的编程要点大概如下:

1、开启GPIO的端口时钟

2、选择要具体控制的IO口,即pin

3、选择IO口输出的速率,即speed

4、选择IO口输出的模式,即mode

5、输出高/低电平

STM32的时钟功能非常丰富,配置灵活,为了降低功耗,每个外设的时钟都可以独自的关闭和开启。STM32中跟时钟有关的功能都由RCC这个外设控制,RCC中有三个寄存器控制着所以外设时钟的开启和关闭:RCC_APHENR、RCC_APB2ENR和RCC_APB1ENR,AHB、APB2和APB1代表着三条总线,所有的外设都是挂载到这三条总线上,GPIO属于高速的外设,挂载到APB2总线上,所以其时钟有RCC_APB2ENR控制。

GPIO 时钟控制
固件库函数:RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE)函数的

原型为:

void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph,
                              FunctionalState NewState)
{
 /* Check the parameters */
 assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
 assert_param(IS_FUNCTIONAL_STATE(NewState));
 if (NewState != DISABLE) 
 {
  RCC->APB2ENR |= RCC_APB2Periph;
 } 
 else 
 {
  RCC->APB2ENR &= ~RCC_APB2Periph;
 }
}

当程序编译一次之后,把光标定位到函数/变量/宏定义处,按键盘的F12或鼠标右键的Go to definition of,就可以找到原型。固件库的底层操作的就是RCC外设的APB2ENR这个寄存器,宏RCC_APB2Periph_GPIOB的原型是:0x00000008,即(1<<3),还原成存器操作就是:RCC->APB2ENR |= 1<<<3。相比固件库操作,寄存器操作的代码可读性就很差,只有才查阅寄存器配置才知道具体代码的功能,而固件库操作恰好相反,见名知意。

GPIO 端口配置

GPIO的pin,速度,模式,都由GPIO的端口配置寄存器来控制,其中IO0~IO7由端口配置低寄存器CRL控制,IO8~IO15由端口配置高寄存器CRH配置。固件库把端口配置的pin,速度和模式封装成一个结构体:

typedef struct 
{
 uint16_t GPIO_Pin;
 GPIOSpeed_TypeDef GPIO_Speed;
 GPIOMode_TypeDef GPIO_Mode;
} GPIO_InitTypeDef;

pin可以是GPIO_Pin_0~GPIO_Pin_15或者是GPIO_Pin_All,这些都是库预先定义好的宏。speed也被封装成一个结构体:

typedef enum 
{
 GPIO_Speed_10MHz = 1,
 GPIO_Speed_2MHz,
 GPIO_Speed_50MHz
} GPIOSpeed_TypeDef;

速度可以是10M,2M或者50M,这个由端口配置寄存器的MODE位控制,速度是针对IO口输出的时候而言,在输入的时候可以不用设置。mode也被封装成一个结构体:

typedef enum 
{
 GPIO_Mode_AIN = 0x0, // 模拟输入
 GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入(复位后的状态)
 GPIO_Mode_IPD = 0x28, // 下拉输入
 GPIO_Mode_IPU = 0x48, // 上拉输入
 GPIO_Mode_Out_OD = 0x14, // 通用开漏输出
 GPIO_Mode_Out_PP = 0x10, // 通用推挽输出
 GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出
 GPIO_Mode_AF_PP = 0x18 // 复用推挽输出
} GPIOMode_TypeDef;

IO口的模式有8种,输入输出各4种,由端口配置寄存器的CNF配置。平时用的最多的就是通用推挽输出,可以输出高低电平,驱动能力大,一般用于接数字器件。

最终用固件库实现就变成这样:

// 定义一个GPIO_InitTypeDef 类型的结构体
GPIO_InitTypeDef GPIO_InitStructure;

// 选择要控制的IO 口
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

// 设置引脚为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

// 设置引脚速率为50MHz
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;

/*调用库函数,初始化GPIOB0*/
GPIO_Init(GPIOB, &GPIO_InitStructure);

倘若同一端口下不同引脚有不同的模式配置,每次对每个引脚配置完成后都要调用GPIO初始化函数,代码如下:

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15 ;                      
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;                  //上拉输入
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 ;                     
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;               //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); 

GPIO 输出控制

GPIO输出控制,可以通过端口数据输出寄存器ODR、端口位设置/清除寄存器BSRR和端口位清除寄存器BRR这三个来控制。端口输出寄存器ODR是一个32位的寄存器,低16位有效,对应着IO0~IO15,只能以字的形式操作,一般使用寄存器操作。

// PB0 输出高电平,点亮LED
GPIOB->ODR = 1<<0;

端口位清除寄存器BRR是一个32位的寄存器,低十六位有效,对应着IO0~IO15,只能以字的形式操作,可以单独对某一个位操作,写1清0。

// PB0 输出低电平,点亮LED
GPIO_ResetBits(GPIOB, GPIO_Pin_0);

BSRR是一个32位的寄存器,低16位用于置位,写1有效,高16位用于复位,写1有效,相当于BRR寄存器。高16位我们一般不用,而是操作BRR这个寄存器,所以BSRR这个寄存器一般用来置位操作。

// PB0 输出高电平,熄灭LED
GPIO_SetBits(GPIOB, GPIO_Pin_0);

综上:固件库LED GPIO初始化函数

void LED_GPIO_Config(void)
{
 // 定义一个GPIO_InitTypeDef 类型的结构体
 GPIO_InitTypeDef GPIO_InitStructure;

 // 开启GPIOB 的时钟
 RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);

 // 选择要控制的IO 口
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

 // 设置引脚为推挽输出
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

 // 设置引脚速率为50MHz
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

 /*调用库函数,初始化GPIOB0*/
 GPIO_Init(GPIOB, &GPIO_InitStructure);

 // 关闭LED
 GPIO_SetBits(GPIOB, GPIO_Pin_0);
}
主函数
#include "stm32f10x.h"


void SOFT_Delay(__IO uint32_t nCount);
void LED_GPIO_Config(void);

int main(void)
{
 // 程序来到main 函数之前,启动文件:statup_stm32f10x_hd.s 已经调用
 // SystemInit()函数把系统时钟初始化成72MHZ
 // SystemInit()在system_stm32f10x.c 中定义
 // 如果用户想修改系统时钟,可自行编写程序修改

 LED_GPIO_Config();

 while ( 1 ) 
 {
  // 点亮LED
  GPIO_ResetBits(GPIOB, GPIO_Pin_0);
  Time_Delay(0x0FFFFF);

  // 熄灭LED
  GPIO_SetBits(GPIOB, GPIO_Pin_0);
  Time_Delay(0x0FFFFF);
 }
}
// 简陋的软件延时函数
void Time_Delay(volatile uint32_t Count)
{
 for (; Count != 0; Count--);
}

注意void Time_Delay(volatile uint32_t Count)只是一个简陋的软件延时函数,如果小伙伴们有兴趣可以看一看MultiTimer,它是一个软件定时器扩展模块,可无限扩展所需的定时器任务,取代传统的标志位判断方式, 更优雅更便捷地管理程序的时间触发时序。

来源:STM32嵌入式开发
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 47

单片机可以替代PLC 吗?

这个问题如同面粉能代替面条一样,答案是否定的。第一次听到这个答案可能很多人都有疑问:

单片机和PLC分别是什么?

它们之间有什么区别?

单片机

单片微型计算机(Single Chip Microcomputer ),亦称微控制单元(Microcontroller Unit),简称MCU,是一种集成电路芯片,是采用超大规模集成电路技术把具有数据处理能力的中央处理器(Central Process Unit;CPU)、随机存储器(Random Access Memory;RAM)、只读存储器(Read-Only Memory;ROM)、多种I/O口和中断系统、定时器/计数器等功能(可能还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电路)集成到一块硅片上构成的一个小而完善的微型计算机系统,在各个领域广泛应用。诸如手机、PC外围、遥控器,至汽车电子、工业上的步进马达、机器手臂的控制等,都可见到MCU的身影。

“入手STM32单片机的知识点总结"

单片机出现的历史并不长,但发展十分迅猛。它的产生与发展和微处理器的产生与发展大体同步,自1971年美国Intel公司首先推出4位微处理器以来,它的发展到目前为止大致可分为5个阶段。

单片机发展的初级阶段(1971年至1976年)

1971年11月Intel公司首先设计出集成度为2000只晶体管/片的4位微处理器Intel 4004, 并配有RAM、 ROM和移位寄存器, 构成了第一台MCS—4微处理器, 而后又推出了8位微处理器Intel 8008, 以及其它各公司相继推出的8位微处理器。

单片机发展的初级阶段(1971年至1976年)

1971年11月Intel公司首先设计出集成度为2000只晶体管/片的4位微处理器Intel 4004, 并配有RAM、 ROM和移位寄存器, 构成了第一台MCS—4微处理器, 而后又推出了8位微处理器Intel 8008, 以及其它各公司相继推出的8位微处理器。

低性能单片机阶段(1976年至1980年)

以1976年Intel公司推出的MCS—48系列为代表, 采用将8位CPU、 8位并行I/O接口、8位定时/计数器、RAM和ROM等集成于一块半导体芯片上的单片结构, 虽然其寻址范围有限(不大于4 KB), 也没有串行I/O, RAM、 ROM容量小, 中断系统也较简单, 但功能可满足一般工业控制和智能化仪器、仪表等的需要。

“入手STM32单片机的知识点总结"

高性能单片机阶段(1980年至1990年)

这一阶段推出的高性能8位单片机普遍带有串行口, 有多级中断处理系统, 多个16位定时器/计数器。片内RAM、 ROM的容量加大,且寻址范围可达64 KB,个别片内还带有A/D转换接口。

16位单片机阶段(1983年至1989年)

1983年Intel公司又推出了高性能的16位单片机MCS-96系列, 由于其采用了最新的制造工艺, 使芯片集成度高达12万只晶体管/片。

全方位高水平发展阶段(1990年至今)

到目前为止,单片机也有从传统的8位处理器平台向32位高级RISC处理器平台转变的趋势,但8位机依然难以被取代。8位单片机成本低,价格廉,便于开发,其性能可以满足大部分的需要,只有在航天、汽车、机器人等高技术领域,需要高速处理大量数据时,才需要选用16/32位,而在一般工业领域,8位通用型单片机,仍然是目前应用最广的单片机。单片机在集成度、功能、速度、可靠性、应用领域等全方位向更高水平发展。

单片机的特点是编程、维护相对复杂,编程方式常用C语言或者汇编语言,成本较低,I/O接口相对有限。

PLC

PLC,全称Programmable Logic Controller,即可编程逻辑控制器,是一种专门为在工业环境下应用而设计的数字运算操作电子系统。它采用一种可编程的存储器,在其内部存储执行逻辑运算、顺序控制、定时、计数和算术运算等操作的指令,通过数字式或模拟式的输入输出来控制各种类型的机械设备或生产过程。

“入手STM32单片机的知识点总结"

单片机为什么不能取代PLC呢?

1. 稳定性与可靠性

有人说这是个伪问题,单片机是元器件,PLC是由元器件以及庞大的软件构成的系统,两者在这一方面没有可比性。这话没有错,大多PLC的控制芯片实际上就是单片机,也就是说可以将PLC看成是单片机的二次开发,单论工业防护等级,单片机的稳定性和可靠性能根本比不了PLC这种IP67类的产品( IP为标记字母,第一标记数字表示接触保护和外来物保护等级,第二标记数字表示防水保护等级)。而且就PLC这种能应对工业恶劣环境的产品还开发出一套冗余系统。如果稳定性与可靠性对比没有意义,那么我们就从其他方面分析。

“入手STM32单片机的知识点总结"

2. I/O功能

单片机的I/O点实在有限,而反观PLC呢?针对不同的现场信号,均有相应的I/O点可与工业现场的器件(如按钮、开关、传感电流变送器、电机启动器或控制阀等)直接连接,并通过总线与CPU主板连接。工业里几乎任意一条生产线,都有上百甚至上千I/O点,就这点单片机完全无法比拟。

“入手STM32单片机的知识点总结"

3. 扩展功能

一条完整的工业生产线除了控制,还有通信、上位、组态、运动控制与显示等等,这些东西都需要依靠完整的工业体系与通信协议去做,例如西门子公司的PROFIBUS-DP通信、三菱重工的CC-LINK等等。而单片机和PC、单片机和单片机之间的通信大都用串口。单片机的串口是全双工异步通信串口,那么像MODBUS、PROFIBUS、CAN open、以太网等通信协议单片机是否能一一实现?或许单片机可以做到,但是这就涉及到下一个分析点,开发周期。

“入手STM32单片机的知识点总结"

4. 开发周期

PLC的品牌多达200多种,几乎每个品牌都有不同编程软件,而且都在不断完善自己的编程软件,使之能够越来越简单的服务于电气工程师,而各种程序块也是越来越方便人性化的任意去调用,比如PID模块、运动控制模块等,大大减轻了工程师的开发压力也缩短了开发周期。那单片机要如何实现?没有现成的模块使用,那就只能开发,那么做过非标自动化设备的工程师都会遇到一个问题——工期不足。PLC这种高度集成化模块化的产品在达到满足设备所需的开发周期,在工期面前也是抓襟见肘,更不用说如同白纸一张的单片机。

5. 通信距离

现在大多数流水线是要跨区域整合与监视的,所用的通讯方式多为以太网加中继器,或者直接走民用宽带光纤,所用的东西到最后很可能是用的就是微软的IE浏览器,很明显PLC是有RJ-45接口,即使本体没有RJ-45也可以配备以太网模块,可单片机搭载的PCB板能加上这个接口然后开发出以太网通信吗?开发需要多久?

“入手STM32单片机的知识点总结"

6. 编程语言

这点对单片机来讲是一个优势,同时也是一个劣势。上面提到PLC的品牌有两百多种,编程软件更多,尽管大多数PLC的编程语言都大同小异,但是每接触一款不同品牌的PLC,电气工程师就要从PLC的硬件参数、软元件、编程软件等等各个方面从头了解一次才能使用的得心应手。而单片机的编程语言用的是C语言或者汇编语言,这对于任何单片机都是通用的。换句话说,学会C语言或者汇编语言,便可以应用任何单片机开发想要的功能(前提是要有相关的电工电子学基础)。但话又说回来,电气工程师不是电子工程师,他们的工作不是单单考虑单片机如何驱动继电器来控制机床的,甚至有的电气工程师都不会C语言、汇编语言之类的MCU开发语言。近些年,IEC-61131-3标准的推广,越来越多的PLC支持多种编程语言,如类似C语言的ST语言,类似电路图的CFC语言。这种便利的功能是传统单片机开发环境真的无法实现。

结论

经过上面阐述,我们可以看出,PLC实际上可以看成是单片机的二次应用开发,但是它又有自己鲜明的特点。到目前为止,中国的单片机应用和嵌入式系统开发走过了二十余年的历程,国民经济建设、军事及家用电器等各个领域,尤其是手机、汽车自动导航设备、PDA、智能玩具、智能家电、医疗设备等行业都是应用了单片机。行业高端目前有超过10余万名从事单片机开发应用的工程师。

来源地址:

https://www.sohu.com/a/274772593_100279726

本文素材来源网络,版权归原作者所有。如不支持转载,请联系删除。

围观 14

单片机复位电路就好比电脑的重启部分,当电脑在使用中出现死机,按下重启按钮电脑内部的程序从头开始执行。单片机也一样,当单片机系统在运行中,受到环境干扰出现程序跑飞的时候,按下复位按钮内部的程序自动从头开始执行。本文介绍的就是单片机按键复位电路原理和电路图解析。

复位电路

在单片机系统中,系统上电启动的时候复位一次,当按键按下的时候系统再次复位,如果释放后再按下,系统还会复位。所以可以通过按键的断开和闭合在运行的系统中控制其复位。

当这个电路处于稳态时,电容起到隔离直流的作用,隔离了+5V,而左侧的复位按键是弹起状态,下边部分电路就没有电压差的产生,所以按键和电容 C11以下部分的电位都是和GND相等的,也就是0V电压。我们这个单片机是高电平复位,低电平正常工作,所以正常工作的电压是0V电压,完全OK,没有问题。

通常的按键分为独立式按键和矩阵式按键两种,独立式按键比较简单,并且与独立的输入线相连接,如下图所示。

“独立式按键电路图"
独立式按键电路图

4条输入线接到单片机的IO口上,当按键K1按下时,+5V通过电阻R1然后再通过按键K1最终进入GND形成一条通路,那么这条线路的全部电压都加到了R1这个电阻上,KeyIn1这个引脚就是个低电平。当松开按键后,线路断开,就不会有电流通过,那么KeyIn1和+5V就应该是等电位,是一个高电平。我们就可以通过KeyIn1这个IO口的高低电平来判断是否有按键按下。

这个电路中按键的原理我们清楚了,但是实际上在我们的单片机IO口内部,也有一个上拉电阻的存在。我们的按键是接到了P2口上,P2口上电默认是准双向IO口,我们来简单了解一下这个准双向IO口的电路,如下图所示。

“准双向IO口结构图"
准双向IO口结构图

当内部输出是高电平,经过一个反向器变成低电平,NPN三极管不会导通,那么单片机IO口从内部来看,由于上拉电阻R的存在,所以是一个高电平。当外部没有按键按下将电平拉低的话,VCC也是+5V,他们之间虽然有2个电阻,但是没有压差,就不会有电流,线上所有的位置都是高电平,这个时候我们就可以正常读取到按键的状态了。

当内部输出是个低电平,经过一个反相器变成高电平,NPN三极管导通,那么单片机的内部IO口就是个低电平,这个时候,外部虽然也有上拉电阻的存在,但是两个电阻是并联关系,不管按键是否按下,单片机的IO口上输入到单片机内部的状态都是低电平,我们就无法正常读取到按键的状态了。

矩阵按键和独立按键的关系

我们在使用按键的时候有这样一种使用经验,当需要多个按键的时候,如果做成独立按键会大量占用IO口,因此我们引入了矩阵按键,如图6所示,使用了8个IO口来实现16个按键。

“单片机按键复位电路原理和电路图"

其实独立按键理解了,矩阵按键也简单,我们来分析一下。图6中,一共有4组按键,我们只看其中一组,如图7所示。大家认真看一下,当KeyOut1输出一个低电平,KeyOut2、KeyOut3、KeyOut4这三个输出高电平时,是否相当于4个独立按键呢。

“单片机按键复位电路原理和电路图"

单片机按键复位电路各元件的作用

“单片机按键复位电路原理和电路图"

如上图,R17 C13组成止电复位电路,刚上电时,C13是电压为0,电源通过R17对电容充电,因此,RST引脚呈现高电平,高电平时间大于2个晶振周期,单片机复位。

电容充电完毕,RST引脚呈现低电平,复位结束。

按钮S22和R16组成手动复位电路 ,按下S22,电源接通R16和 R17,由于R17阻值比较大,因此RST是高电平,同时电容通过R16迅速放电,即使按钮触点断开,电源也可对C13充电,使RST高电平稳定一段时间 ,保证可靠复位。C13容量较小时,R16可省掉,小电容短路放电不会损坏按钮触点。

来源:STM32嵌入式开发
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 13

页面

订阅 RSS - 单片机