I/O基本结构
三种输入方式:
(1)一般数字输入
所走路径为①到④,间要经过“斯密特触发器”和“输入数据寄存器”。
a.斯密特触发器的作用
由于从引脚输入的高低电平信号不是很完美,所以需要经过斯密特触发器的修整,经过修整之后信号就会变得更稳定、更漂亮。
b.输入寄存器的作用
输入的数字信号(数据)将缓存在“输入数据寄存器”中,然后程序即可从“输入数据寄存器”中读出输入的数据,拿到数据后,程序就可以根据需要来使用这个数据了。
(2)复用功能输入:
所走路径为①到③,其输入的数字信号经过“斯密特触发器”的修整之后,就可以交给复用该引脚的其它“片内外设”的寄存器保存起来了,比如 USB、DMA、UART 等,这些“片内外设”拿到数据后,就会按照自己的要求来使用这些数据。
使用复用输入时,除了要配置 GPIO 的寄存器外,还需要配置 USB、DMA、UART 这些外设的寄存器,让这些“片内外设”工作起来后,它们才能接收“复用输入”的数据。
(3)模拟输入
所走路径为①到②,因为模拟信号不需要修正,所以不经过“斯密特触发器”。模拟信号输入进到芯片内部后,就会交给处理模拟信号的“片内外设”,比如交给 AD,AD 就会进行模拟/数字的转换,将模拟信号变为数字信号。
什么时候使用模拟输入
比如,温度传感器将温度转为模拟电信号后,没有将模拟信号变为数字信号,此时芯片为了能够处理这个模拟信号,就需要先通过模拟输入得到模拟信号,然后芯片内部的 AD 再将其转为数字信号,然后才能使用这个数字信号温度值。
不过这种情况不多见,因为现在大多数的传感器都自带 AD,会直接将模拟信号转为数字信号,最终给芯片的其实都已经是数字信号了
三种输入路径:
(1)一般的数字输出
路径为⑤①或者⑥①,这两种其实是一样的,唯一不同的是,
- ⑤①路径:程序将数据先讲写入“复位/置位寄存器”(标记 e),然后电路再自动将数据导入“输出数据寄存器”(标记 f)中,然后在输出。
- ⑥①路径:程序直接将数据写入“数据输出寄存器”,然后再输出。
问1:为什么在 ⑤①路径中,数据要先写入“复位/置位寄存器”?
涉及到“复位/置位寄存器”和“输出数据寄存器”之间区别
问2:为什么还可以读“输出数据寄存器”?
我们举例理解,假如我想将原来“输出数据寄存器”中数据的第 3bit 改为 0 后再输出,可是我并不知道原来里面的值是多少,此时应该怎么办呢?我们的程序就可以先读出“输出数据寄存器”中原来的数据,然后将第 3bit 改为 0,然后再重新回写到“输出数据寄存器”中。
PS:
初看这张图的很多人总有一种误区,总以为每个引脚都有完全独立的一套寄存器,其实不是这样的,真实的情况是每个端口的 16 个引脚,比如 GPIOB 端口的 16 个引脚,它们共用相同的“输入数据寄存器”、“输出数据寄存器”、“复位/置位寄存器”等寄存器,不过每个端口的寄存器肯定都是独立的,比如 GPIOA 端口有自己独立的“输入数据寄存器”,GPIOB、GPIOC 等端口也有自己独立的“输入数据寄存器”。
问3:图中的“输出控制”是个啥?(标记 g)
“输出控制”这个东西用于选择“输出类型”,数字信号的输出可以有两种“输出类型”,一种为“推挽输出”,另一种为“开漏输出”。每个 GPIOx 端口都有提供专门的寄存器,以供我们选择是使用“推挽输出”,还是使用“开漏输出”。
(2)复用输出
输出路径为⑦①,复用输出时可以是“推挽输出”的或者“开漏输出”的,看需求。
假设引脚使用的是 LCD 相关的复用,此时程序会先将图像数据交给“LCD 片内外设”,片内外设再将数据通过“复用输出路径”将输出从引脚输出,最终将数据交给 LCD 液晶以供显示。
(3)模拟输出
输出路径为⑧①,我们举一个会用到模拟输出的例子,比如芯片内部的 DA 将数字信号转为了模拟信号,此时片内模拟信号的输出,走的就是“模拟输出路径”。
端口配置表
1.硬件:
开发板:
GPIO负极接地
IO->1 亮
IO->2 灭
2.MDK工程建立
注意:单片机中用到的C语言其实不是标准的,而是有点定制性的C语言。
之前认识:整个程序从main函数开始执行,main执行完了整个程序就完了。
起始代码:从CPU复位开始执行的第1句指令,到main函数之前所做的事情就是起始代码。
3.关于起始代码
(1)起始代码是哪里来的
(2)不同的CPU的其实代码一般是不同的
(3)起始代码是用汇编写的,一般不需要看懂,知道点就行了
3.寄存器信息确认
PG6 PG7
(1)STM32 PortG的起始地址是:0x4001 2000
(2)有可能涉及到的GPIO的地址:
寄存器名 偏移量 寄存器地址
GPIOG_CRL 0x00 0x40012000
GPIOG_CRH 0x04 0x40012004
GPIOG_IDR 0x08 0x40012008 //输入模式寄存器
GPIOG_ODR 0x0C 0x4001200C 输出模式寄存器
GPIOG_BSRR 0x10 0x40012010 ? 置位/复位寄存器
GPIOG_BRR 0x14 0x40012014 ? 复位寄存器
4.C语言操作寄存器
(1)ARM是内存与IO统一编址的,所以ARM中的所有外设都是通过寄存器的方式来操作的
(2)每个寄存器都有地址,C语言通过这些地址来操作这些寄存器位,用到的C语言技巧主要是C语言的位操作和C语言指针。
(3)常见面试题:用C语言向内存地址0x30000004写入16
(unsigned int )0x30000004 = 16; 或者:
unsigned int p = (unsigned int )0x30000004; *p = 16;
原始代码:
#define GPIOG_CRL ((unsigned int *)0x40012000)
#define GPIOG_CRH ((unsigned int *)0x40012004)
#define GPIOG_IDR ((unsigned int *)0x40012008)
#define GPIOG_ODR ((unsigned int *)0x4001200C)
#define GPIOG_BSRR ((unsigned int *)0x40012010)
#define GPIOG_BRR ((unsigned int *)0x40012014)
#define RCC_APB2ENR ((unsigned int *)0x40021018)
int main(void)
{
//使能GPIOG
*RCC_APB2ENR = 0x00000100;
// 向CRH寄存器写内容,将GPIOG6-7配置为输出模式
// 推挽输出模式,输出模式为50MHZ
*GPIOG_CRL = 0x33333333;
// 将GPIOG6 GPIOG7设置为1
//*((unsigned int *)GPIOG_ODR) = 0x000000F0;
//通过BSRR寄存器置1
*GPIOG_BSRR = 0x0000000C0;
while(1);
}
代码优化:
gpio.h
#ifndef H_GPIO_G
#define H_GPIO_G
typedef unsigned int u32;
#define PERIPH_BASE 0x40000000
#define GPIOG_BASE (PERIPH_BASE + 0x12000)
#define RCC_APB2ENR ((unsigned int *)0x40021018)
typedef struct
{
u32 CRL; //*
u32 CRH; //*
u32 IDR; //*
u32 ODR; //*
u32 BSRR;
u32 BRR; //*
}GPIOG_TypeDef;
#define GPIOG ((GPIOG_TypeDef *)GPIOG_BASE)
#endif
main.c
#include "gpio.h"
int main(void)
{
//使能GPIOG
*RCC_APB2ENR = 0x00000100;
// 向CRH寄存器写内容,将GPIOG6-7配置为输出模式
// 推挽输出模式,输出模式为50MHZ
GPIOG->CRL = (0x33 << 24);
GPIOG->BSRR = 0x0000000C0;
while(1);
}
闪烁
#include "gpio.h"
void Delay(void);
void Delay(void)
{
unsigned int i = 0, j = 0;
for( i = 0; i < 1000; i++)
{
for( j = 0; j < 2000; j++);
}
}
int main(void)
{
//使能GPIOG
RCC_APB2ENR = 0x00000100;
// 向CRH寄存器写内容,将GPIOG6-7配置为输出模式
// 推挽输出模式,输出模式为50MHZ
GPIOG->CRL = (0x33 << 24);
while(1)
{
GPIOG->BSRR = 0x000000080;
Delay();
GPIOG->BSRR = 0x00000040;
Delay();
GPIOG->BSRR = 0x00ff0000;
Delay();
}
}
5.时钟设置和函数移植
5.1、时钟模块回顾
(1)一个疑惑:前面代码并没有设置时钟为什么能够运行->默认启动就会使用 内部的8MHz
(2)时钟框图
5.2、时钟设置示例代码分析
(1)相关寄存器及定义
(2)代码详解
RCC->CR 就相当于是rRCC_APB2ENR
clock.h
#ifndef __CLOCK_H__
#define __CLOCK_H__
#include "gpio.h"
// 寄存器宏定义
// RCC寄存器基地址为0x40021000
#define RCC_BASE 0x40021000 // RCC部分寄存器的基地址
#define RCC_CR (RCC_BASE + 0x00) // RCC_CR的地址
#define RCC_CFGR (RCC_BASE + 0x04)
#define FLASH_ACR 0x40022000
// 用C语言来访问寄存器的宏定义
#define rRCC_CR (*((volatile unsigned int *)RCC_CR))
#define rRCC_CFGR (*((volatile unsigned int *)RCC_CFGR))
#define rFLASH_ACR (*((volatile unsigned int *)FLASH_ACR))
// 函数作用:时钟源切换到HSE并且使能PLL,将主频设置为72MHz
void Set_SysClockTo72M(void);
#endif
clock.c
如果出错了,通过LED的亮和灭来调试信息
#include "clock.h"
void Set_SysClockTo72M(void)
{
unsigned int rccCrHserdy = 0;
unsigned int rccCrPllrdy = 0;
unsigned int rccCfrSwsPll = 0;
unsigned int faultTime = 0;
rRCC_CR = 0x00000083;
rRCC_CR &= ~(1<<16); // 关闭HSEON
rRCC_CR |= (1<<16); // 打开HSEON,让HSE工作
do
{
rccCrHserdy = rRCC_CR & (1<<17); //检测第17位是否为1
faultTime++;//检测时间
}
while ((faultTime<0x0FFFFFFF) && (rccCrHserdy==0));
if ((rRCC_CR & (1<<17)) != 0)
{
rFLASH_ACR |= 0x10;
rFLASH_ACR |= (0x02);
// 到这里HSE就ready了,下面再去配PLL并且等待他ready
rRCC_CFGR &= (~((0x0f<<4) | (0x07<<8) | (0x07<<11)));
//rRCC_CFGR &= (~(0x3ff<<4));
// AHB和APB2未分频,APB1被2分频,所以最终:AHB和APB2都是72M,APB1是36M
rRCC_CFGR |= ((0x0<<4) | (0x04<<8) | (0x0<<11));
// 选择HSE作为PLL输入并且HSE不分频,所以PLL输入为8M
rRCC_CFGR &= (~((1<<16) | (1<<17))); // 清零bit17和bit16
rRCC_CFGR |= ((1<<16) | (0<<17)); // 置1 bit16
// 设置PLL倍频系数为9
rRCC_CFGR &= (~(0x0f<<18)); // 清零bit18-21
rRCC_CFGR |= (0x07<<18); // 9倍频
// 打开PLL开关
rRCC_CR |= (1<<24);
// do while 循环等待PLL时钟稳定
faultTime = 0;
do
{
rccCrPllrdy = rRCC_CR & (1<<25); //检测第25位是否为1
faultTime++;//检测时间
}
while ((faultTime<0x0FFFFFFF) && (rccCrPllrdy==0));
//while (rccCrPllrdy==0);
if ((rRCC_CR & (1<<25)) == (1<<25))
{
// 到这里说明PLL已经稳定了,可以用了,下面就可以切了
// 切换PLL输出为SYSCLK
rRCC_CFGR &= (~(0x03<<0));
rRCC_CFGR |= (0x02<<0);
faultTime = 0;
do
{
rccCfrSwsPll = rRCC_CFGR & (0x03<<2); //检测第25位是否为1
faultTime++;//检测时间
}
while ((faultTime<0x0FFFFFFF) && (rccCfrSwsPll!=(0x02<<2)));
if ((rRCC_CFGR & (0x03<<2))== (0x02<<2))
{
// 到这里我们的时钟整个就设置好了,可以结束了
}
else
{
// 到这里就说明PLL输出作为SYSCLK不成功
while (1);
}
}
else
{
// 到这里就说明PLL启动时出错了,PLL不能稳定工作
while (1);
}
}
else
{
// HSE配置超时,说明HSE不可用,一般硬件就有问题要去查
while (1);
}
}
6.编程总结
6.1、STM32和51或其他简单单片机的相同
(1)开关环境都是Keil
(2)都是看原理图和数据手册
(3)都是用C语言
6.2、STM32和51或其他简单单片机的不同
(1)工程会更复杂,会用到Keil的一些高级设置
(2)原理图和数据手册比简单单片机更复杂(复杂不是难)
(3)STM32会用到C语言的更多高级特性
6.3、外设编程思路
(1)都是套路
(2)会出现问题,这时候就需要调试能力(不一定非要调试器)
(3)注意熟悉和体会这种套路