0%

•中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行。

•中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。(紧急可以把优先级设高,不紧急可以设低)

•中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。(把中断程序再次中断)
中断

STM32中断

•68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设

•使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级

NVIC基本结构

NVIC基本结构
用来统一分配中断优先级和管理中断。
NVIC根据每个中断的优先级。(类似叫号系统)

NVIC优先级分组

•NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级

•抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队

一般紧急:响应优先级类似插队(后来但是先处理)
更紧急:抢占优先级,中断现在处理的任务,先处理。

分组方式抢占优先级响应优先级
分组00位,取值为04位,取值为0~15
分组11位,取值为0~13位,取值为0~7
分组22位,取值为0~32位,取值为0~3
分组33位,取值为0~71位,取值为0~1
分组44位,取值为0~150位,取值为0

值越小,优先级越高。

EXTI简介

•EXTI(Extern Interrupt)外部中断

•EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序(引脚电平变化,申请中断)

•支持的触发方式:上升沿/下降沿/双边沿/软件触发

•支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断(比如PA0和PB0不能同时用,端口Pin一样的)

•通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒

触发响应方式:中断响应/事件响应
中断响应:申请中断,让CPU执行中断函数。
事件响应:触发事件,外部中断信号不会通向CPU,而是通向其他外设,比如出发ADC转换、DMA等。
EXTI简介

AFIO复用IO口

•AFIO主要用于引脚复用功能的选择和重定义

•在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择
AFIO复用IO口

EXTI框图

EXTI框图

Keil 调试模式1
电机放大镜d按钮,进入调试模式。

Keil 调试模式2
红色框住的窗口,是C语言翻译成的汇编程序。
绿色窗口,是寄存器组和状态标志位信息。
粉色窗口,为C语言程序。

Keil 调试模式3
上图为程序运行控制栏。
依次为复位、全速运行、停止全速运行。
单步运行、跳过当前行单步运行、跳出当前函数单步运行、跳到光标指定位置单步运行。

Keil 调试模式4
这个按钮是符号窗口,在这里可以查看到所有寄存器的值。
Keil 调试模式5
想要看某个寄存器的值,可以右键,添加到观察栏。

Keil 调试模式6
这里可以看到所有外设信息,比如选择GPIOA,就可以看到GPIOA外设所有寄存器。
ODR寄存器是输出寄存器,ODR0,就是PA0的输出寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"


int main(void)
{
OLED_Init();
OLED_ShowChar(1,1,'A');
OLED_ShowString(1,3,"Hello World!");
OLED_ShowNum(2,1,12345,5);
OLED_ShowSignedNum(2,7,-66,2);
OLED_ShowHexNum(3,1,0xAA55,4);
OLED_ShowBinNum(4,1,0xAA55,16);
//OLED_Clear();
}

为什么在OLED(I2C接口)驱动中,推挽输出模式(PP)能正常工作,而开漏输出模式(OD)不能,即使I2C协议标准要求开漏输出?

因为OLED模块内部可能没有上拉电阻,或者上拉电阻的阻值过大

1. I2C协议与开漏输出的标准配置

  • 理论上:I2C总线是一个多主多从、双向、半双工的串行总线。它通过两条线通信:

    • SDA(串行数据线):传输数据。

    • SCL(串行时钟线):提供时钟信号。

  • 开漏输出(Open-Drain)的原理

    • 在OD模式下,GPIO引脚内部的MOSFET只能将线路拉低到地(GND),或者高阻态释放

    • 无法主动输出高电平(VCC)

    • 线路上的高电平必须依靠外部上拉电阻将电压拉至VCC。

  • 为什么I2C标准要求开漏输出?

    • 防止总线冲突:如果多个设备同时向总线输出,一个输出高(推挽),一个输出低(开漏),会造成短路。开漏模式只能拉低,不能主动拉高,避免了电源间的直接冲突,实现了“线与”功能。

    • 电平兼容:不同设备的供电电压可能不同(如3.3V和5V)。只要上拉电阻接到合适的电压,开漏模式可以方便地进行电平转换。

    • 总线可以双向通信:SDA线需要被主机和从机共同驱动。开漏模式使得任何一方都可以安全地拉低线路。

2. 你的实际情况:为什么OD不工作,PP却工作?

  • OD模式不工作(无显示)

    • 当你的PB8/PB9配置为开漏输出时,单片机只能将SDA/SCL线拉低。

    • 当单片机释放总线(输出高电平)时,引脚处于高阻态。此时,线路上的电压完全依赖于外部上拉电阻

    • 如果你的OLED模块没有集成上拉电阻,或者电阻值非常大(例如>100kΩ),那么上拉能力极弱。 导致的结果是:

      1. 时钟线SCL无法上升到稳定的高电平:时钟信号失真,OLED无法正确识别时钟边沿。

      2. 数据线SDA的高电平建立缓慢:在高速通信下,电平还没升到逻辑“1”就被采样了,导致数据错误。

      3. 总体表现:I2C通信失败,初始化或数据传输都无法完成,屏幕自然没有任何显示。用逻辑分析仪或示波器看,会看到波形“爬坡”非常缓慢,幅值不足。

  • PP模式正常工作

    • 当配置为推挽输出时,GPIO内部有PMOS和NMOS两个MOSFET。

    • 输出高电平时,PMOS导通,直接连接到VDD(3.3V),主动驱动线路为高电平。

    • 输出低电平时,NMOS导通,将线路拉低到地。

    • 在这种情况下,即使总线上没有外部上拉电阻,单片机也能靠自身强力输出稳定的高电平和低电平,保证了通信波形的完整性,因此OLED可以正常初始化和显示。

结论:根本原因是你使用的硬件电路(单片机板+OLED模块)组合缺少了I2C总线必需的外部上拉电阻

解决方案(从优到次)

  1. 最佳实践 - 添加上拉电阻并改回OD模式

    • 在 PB8(SCL)和 PB9(SDA) 各自连接到 VCC(3.3V) 的线上,添加一个4.7kΩ - 10kΩ的电阻

    • 将GPIO模式改回 GPIO_MODE_OUTPUT_OD (开漏输出)。

    • 这是最标准、最安全、最兼容I2C协议的做法。它允许总线被多个设备驱动,也符合电平规范。

  2. 临时方案 - 继续使用PP模式(不推荐长期使用)

    • 如果你只是单个主机驱动单个OLED从机,并且没有其他设备挂载到这两根线上,推挽模式在短期内可以“凑合用”

    • 严重缺点

      • 破坏了I2C的“线与”特性,不能再挂载其他I2C设备,否则有短路风险。

      • 如果OLED模块内部有弱上拉,可能会形成电流冲突,长期可能损坏器件。

      • 代码移植到标准硬件上可能反而出问题。

OLED

函数作用
OLED_Init();初始化
OLED_Clear();清屏
OLED_ShowChar(1, 1, ‘A’);显示一个字符
OLED_ShowString(1, 3, “HelloWorld!”);显示字符串
OLED_ShowNum(2, 1, 12345, 5);显示十进制数字
OLED_ShowSignedNum(2, 7, -66, 2);显示有符号十进制数字
OLED_ShowHexNum(3, 1, 0xAA55, 4);显示十六进制数字
OLED_ShowBinNum(4, 1, 0xAA55, 16);显示二进制数字

OLED

1
2
3
4
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//用来读取输入数据寄存器某一个位的输入值
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);//读取整个输入数据寄存器
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//用来读取输出数据寄存器某一个位的输入值
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);//读取整个输出数据寄存器

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "stm32f10x.h"                  // Device header
#include "delay.h"
#include "LED.h"
#include "key.h"

uint8_t KeyNum;//这里定义的是全局变量,和key.c中定义的局部变量是不一样的
int main(void)
{
LED_Init();
KEY_Init();
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1)
{
LED1_Turn();
}
else if(KeyNum == 2)
{
LED2_Turn();
}
}
}

LED.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include "stm32f10x.h"                  // Device header

void LED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_1 | GPIO_Pin_2);
}

void LED1_ON(void)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1);
}
void LED1_OFF(void)
{
GPIO_SetBits(GPIOA,GPIO_Pin_1);
}
void LED1_Turn(void)
{
if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_1) == 0)//读取PA1的值,类似LD=,做判断用
{
GPIO_SetBits(GPIOA,GPIO_Pin_1);
}
else
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1);
}
}
void LED2_ON(void)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_2);
}
void LED2_OFF(void)
{
GPIO_SetBits(GPIOA,GPIO_Pin_2);
}
void LED2_Turn(void)
{
if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_2) == 0)
{
GPIO_SetBits(GPIOA,GPIO_Pin_2);
}
else
{
GPIO_ResetBits(GPIOA,GPIO_Pin_2);
}
}

key.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
void KEY_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//ÒòΪҪ¶ÁÈ¡°´¼ü£¬Ñ¡ÔñÉÏÀ­ÊäÈë
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}

uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if ( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0 )
{
Delay_ms(20);//延时20ms因为会抖动
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0)
{
Delay_ms(20);
KeyNum = 1;
}
}
if ( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) == 0 )
{
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) == 0)
{
Delay_ms(20);
KeyNum = 2;
}
}
return KeyNum;
}

按键:常见的输入设备,按下导通,松手断开。
按键抖动:由于按键内部使用的是机械式弹簧片进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动。

传感器模块:传感器元件(光敏电阻/热 敏电阻/红外接收管等)的电阻会随外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出。
GPIO输入
一般按键使用下接的方式,和LED的接法类似,是习惯和规范。

LED闪烁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "stm32f10x.h"                  // Device header
#include "delay.h"
int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
while(1)
{
//GPIO_ResetBits(GPIOA,GPIO_Pin_0);
//Delay_ms(500);
//GPIO_SetBits(GPIOA,GPIO_Pin_0);
//Delay_ms(500);
//GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
//Delay_ms(500);
//GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
//Delay_ms(500);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)0);
Delay_ms(500);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)1);
Delay_ms(500);
}
}

(BitAction)0为强制类型转换,把1和0类型转换为BitAction的枚举类型。
LED无论是长脚插在PA0口(高电平),还是短脚插在PA0口(低电平),都会闪烁,说明:在推挽模式下,高低电平都有驱动能力。

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
改为GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
只能短脚插在PA0,灯会闪烁。长脚插PA0,灯灭。
说明开漏模式的高电平是没有驱动能力的,相当于高阻态。

LED流水灯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "stm32f10x.h"                  // Device header
#include "delay.h"
int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
while(1)
{
GPIO_Write(GPIOA,~0x0001);//0000 0000 0000 0001
Delay_ms(500);
GPIO_Write(GPIOA,~0x0002);//0000 0000 0000 0001
Delay_ms(500);
GPIO_Write(GPIOA,~0x0004);//0000 0000 0000 0001
Delay_ms(500);
GPIO_Write(GPIOA,~0x0008);//0000 0000 0000 0001
Delay_ms(500);
GPIO_Write(GPIOA,~0x0010);//0000 0000 0000 0001
Delay_ms(500);
GPIO_Write(GPIOA,~0x0020);//0000 0000 0000 0001
Delay_ms(500);
GPIO_Write(GPIOA,~0x0040);//0000 0000 0000 0001
Delay_ms(500);
GPIO_Write(GPIOA,~0x0080);//0000 0000 0000 0001
Delay_ms(500);
}
}

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;可以通过按位或的方式使能多个GPIO口。

蜂鸣器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "stm32f10x.h"                  // Device header
#include "delay.h"
int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
while(1)
{
GPIO_SetBits(GPIOB,GPIO_Pin_12);
Delay_ms(100);
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
Delay_ms(100);
GPIO_SetBits(GPIOB,GPIO_Pin_12);
Delay_ms(700);
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
Delay_ms(100);
}
}

GPIO简介

•GPIO(General Purpose Input Output)通用输入输出口
•可配置为8种输入输出模式
•引脚电平:0V~3.3V,部分引脚(FT)可容忍5V
•输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等
•输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等
Start目录下的文件

GPIO基本结构

在STM32中,所有GPIO挂载在APB2总线。
每个GPIO有16个引脚,编号从0-15。
GPIO基本结构
在每个GPIO模块中,主要包含寄存器和驱动器,内核通过APB2总线对内核进行读写。
输出寄存器写1,对应的引脚输出高电平,写0,输出低电平。
输入寄存器写1,对应端口目前为高电平,写0,为低电平。
这个寄存器的低16位对应端口,高16位没有使用。

GPIO位结构

GPIO位结构
上面是输入部分,下面为输出部分。

输入部分:

I/O引脚,两个保护二极管,主要是对输入电压进行限幅的。
输入驱动和器连接上拉电阻和下拉电阻。上拉电阻连接VDD,下拉电阻连接VSS。开关通关程序进行配置。如果上面导通,下面断开,为上拉输入模式,如果上面断开,下面导通,为下拉输入模式,如果都断开,为浮空输入模式。
TTL肖特基触发器(施密特触发器),对输入电压进行整形。如果输入电压大于某一阈值,输出为高电平。输入电压小于某一阈值,输出为低电平。
经过施密特触发器整形的波形,可以直接写入数据寄存器。
上面的2路输入,连接到片上外设。模拟输入,连接到ADC上。为模拟量,在施密特触发器前面。
复用功能输入,连接到其他需要读取端口的外设上。为数字量,在施密特触发器后面。

输出部分

数字部分由输出寄存器或片上外设控制。
选择输出寄存器控制,就是普通的IO口输出。写这个数据寄存器的某一位可以操作端口。
经过输出控制,接到2个MOS管,可以选择推挽、开漏或关闭3种模式。

在推挽输出模式下,P-MOS和N-MOS均有效。
数据寄存器为1时,上管导通,下管断开。输出接到VDD,输出高电平。
数据寄存器为0时,下官导通,上管断开,输出接到VSS,输出低电平。
在推挽输出模式下,STM32对IO口有绝对控制权。

在开漏输出模式下,P-MOS无效,只有N-MOS工作。
数据寄存器为1时,下管断开,输出相当于断开,为高阻模式。
数据寄存器为0时,下管导通,输出直接接到VSS,输出低电平。
这种模式下,只有低电平有驱动能力,高电平没有驱动能力。
开漏模式可以作为通讯协议的驱动方式,比如I2C通信。
开漏模式还可以输出5V电平的信号。

在关闭模式下,两个MOS管都无效,输出关闭,端口的电平由外部信号控制。

GPIO模式

•通过配置GPIO的端口配置寄存器,端口可以配置成以下8种模式

模式名称性质特征
浮空输入数字输入可读取引脚电平,若引脚悬空,则电平不确定
上拉输入数字输入可读取引脚电平,内部连接上拉电阻,悬空时默认高电平
下拉输入数字输入可读取引脚电平,内部连接下拉电阻,悬空时默认低电平
模拟输入模拟输入GPIO无效,引脚直接接入内部ADC
开漏输出数字输出可输出引脚电平,高电平为高阻态,低电平接VSS
推挽输出数字输出可输出引脚电平,高电平接VDD,低电平接VSS
复用开漏输出数字输出由片上外设控制,高电平为高阻态,低电平接VSS
复用推挽输出数字输出由片上外设控制,高电平接VDD,低电平接VSS

.\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm目录下的文件为启动文件,复制到工程目录,新建Start文件夹,并粘贴启动文件。

.\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x
目录下的stm32f10x.h是STM32的外设寄存器描述文件,用于描述STM32有哪些寄存器和它对应的地址。
system_stm32f10x.csystem_stm32f10x.h主要是用来配置时钟的。
这三个文件粘贴到Start文件夹下。

因为STM32是内核和内核外围的设备组成的,且内核的寄存器描述文件和外围设备的描述文件不是在一起的,还需要添加内核寄存器描述文件。
.\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\CoreSupport目录下的core_cm3.ccore_cm3.h就是内核寄存器描述,还带了内核的配置函数。
这两个文件粘贴到Start文件夹下。

在Keil软件中,选择添加存在的文件,添加启动文件(startup_stm32f10x_md.s),和剩下的.c和.h文件。
Start目录下的文件
最后还要再工程选项里添加上Start文件夹的头文件路径,点击魔术棒,进入C/C++,在Include Paths栏中,把Start路径添加进去。

接下来新建User文件夹,在工程目录中新建User文件夹,然后再Keil中,Target点击右键,添加组,改个名字,叫User,在User右键,点击添加新文件,选择C文件,名字叫main。

⚠ 路径选择User文件夹,否则默认放在工程根目录。

右键选择插入头文件,选择stm32f10x.h

⚠ 最后一行必须为空行,否则会报警告。

📖点击扳手按钮,在Editor下,Encoding编码格式选为UTF-8,否则可能会出现乱码问题。

配置调试器:点击魔术棒按钮,Debug,默认的调试器为ULINK,改为STLINK。再点击右侧的Settings按钮,在Flash Download下,勾选Reset and Run。选择此项,下载程序后会立刻复位并运行,方便调试。

在工程目录下新建文件夹,LIbrary,来存放库函数。
.\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\src目录下,misc.c为内核的库函数,其他为内核外的外设库函数,全部复制到Library文件夹下。
再打开.\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\inc,该目录下的内容为库函数的头文件,全部复制到Library文件夹下。
在keil软件添加组,改名Library,添加存在的文件,添加Library下全部文件。

进入\STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template目录,复制stm32f10x_conf.hstm32f10x_it.cstm32f10x_it.h。粘贴到User目录下。

stm32f10x_conf.h用于配置库函数头文件包含关系和检查函数定义。
it文件用来存放中断函数。

回到Keil软件,添加刚才的3个文件。

打开Keil工程选项(魔术棒),在C/C++中Define添加USE_STDPERIPH_DRIVER用来包含标准外设库。

在Include Paths中,添加User和Library文件夹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stm32f10x.h"                  // Device header

int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);//用来使能或失能APB2的外设时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);//配置端口模式,第一个是选择哪个GPIO,第二个是选择结构体
// GPIO_SetBits(GPIOC,GPIO_Pin_13);//将PC13口置为高电平
GPIO_ResetBits(GPIOC,GPIO_Pin_13);//将PC13口置为低电平
}

新建工程步骤
•建立工程文件夹,Keil中新建工程,选择型号
•工程文件夹里建立Start、Library、User等文件夹,复制固件库里面的文件到工程文件夹
•工程里对应建立Start、Library、User等同名称的分组,然后将文件夹内的文件添加到工程分组里
•工程选项,C/C++,Include Paths内声明所有包含头文件的文件夹
•工程选项,C/C++,Define内定义USE_STDPERIPH_DRIVER
•工程选项,Debug,下拉列表选择对应调试器,Settings,Flash Download里勾选Reset and Run

工程架构

在信捷PLC编程中,ZCP(区间比较)和CMP(单值比较)都是数据比较指令,但功能和应用场景有显著区别。以下是详细对比及实例说明:


1. 核心区别

指令功能输出结果数量适用场景
CMP比较源数据与单个目标值3种结果(>、=、<)判断数据是否超限、等于特定值
ZCP判断源数据是否在指定区间内3种结果(<下限、区间内、>上限)数据范围检测(如温度、压力控制)

2. 指令详解

(1) CMP 指令

  • 语法CMP S1 S2 D
    • S1:源数据(待比较值)
    • S2:目标值
    • D:连续3个位元件存储结果(D=小于, D+1=等于, D+2=大于)
  • 执行特点:每个扫描周期执行一次,结果实时更新。

(2) ZCP 指令

  • 语法ZCP S1 S2 S3 D
    • S1:区间下限
    • S2:区间上限(需满足 S1 ≤ S2
    • S3:源数据
    • D:连续3个位元件存储结果(D=小于下限, D+1=区间内, D+2=大于上限)
  • 执行特点:仅在使能条件ON时执行一次(需配合上升沿触发)。

3. 实例演示

场景1:CMP 指令应用(速度监控)

1
2
3
4
5
|   X0      CMP K100 D0 M0  |  
|----|------[CMP 100 D0 M0]-|
| |--[M0]----(Y0) | // D0 < 100 时Y0亮
| |--[M1]----(Y1) | // D0 = 100 时Y1亮
| |--[M2]----(Y2) | // D0 > 100 时Y2亮
  • 功能:监控D0的值:
    • D0 < 100M0=ON → Y0输出
    • D0 = 100M1=ON → Y1输出
    • D0 > 100M2=ON → Y2输出

场景2:ZCP 指令应用(温度区间控制)

1
2
3
4
5
|   X1      ZCP K50 K80 D10 M10  |  
|----|------[ZCP 50 80 D10 M10]-|
| |--[M10]---(Y3) | // D10 < 50 → 温度过低
| |--[M11]---(Y4) | // 50≤D10≤80 → 温度正常
| |--[M12]---(Y5) | // D10 > 80 → 温度过高
  • 功能:检测D10(温度值):
    • <50℃M10=ON → 启动加热(Y3)
    • 50~80℃M11=ON → 维持状态(Y4)
    • >80℃M12=ON → 启动冷却(Y5)

4. 关键注意事项

  1. 结果元件占用
    • CMP/ZCP 的输出会占用连续3个位元件(如指定M0则实际使用M0~M2)。
  2. 指令触发方式
    • CMP:每个扫描周期执行,适合实时监控。
    • ZCP:需配合上升沿触发(如LDP X1),避免多次执行。
  3. 区间有效性
    • ZCP 要求 S1 ≤ S2,否则 M11(区间内)永远为OFF。

总结

  • CMP:用于单点比较(如阈值报警、数值匹配)。
  • ZCP:用于区间判断(如工艺参数范围控制)。
    根据实际需求选择指令,可高效实现精准的逻辑控制。