stm32标准库学习笔记
——基于STM32F10x标准库中文数据手册、野火指南者库开发教程、CSDN部分blog内容,针对比赛失败经验的有方向性、有目的性的学习笔记
/**
* @file stm32标准库系统学习笔记
* @author jUicE_g2R(qq:3406291309)————彬(bin-必应)
* 某双流一大学通信与信息专业大一在读的技术彩笔
*
* @brief 研究stm32一些底层的东西(基于标准库与少量寄存器配置)
*
* @note 参考STM32F10x标准库中文数据手册、野火指南者教程、CSDN部分blog内容
* @note 便于开发时资料的查询
*
* @copyright 2023.5
* @COPYRIGHT © 版权所有 原创技术笔记:转载需获得博主本人同意,且需标明转载源
*
* @Version 1.0还在学习中
*/
目录
文件夹名 | 存放的文件 |
---|---|
Doc | ReadMe,一些工程参考资料 |
Libraries | 官方库的source(.c文件)、include(.h文件),startup(.s启动的汇编文件) |
HardWare | 用户编写的片外外设配置文件:如KEY_Confog.c(.h) |
Peripheral | 用户编写的片上外设配置文件:如USART_Config.c(.h) |
System | 用户编写的控制系统内核(外设)的配置文件SysTick.c(.h)与该项目的主控核心 |
Project | 存放HEX烧录文件和(.uvproj)工程启动文件 |
User | 主控文件main.c(或.h)与stm32f10x_config.h与stm32f10x_it.c(.h) |
keilkill.bat |
注1:startup根据容量(64k,128k,512k)选择
注2:keil建文件时对芯片的选型不要错了
注3:工程的文件存放具有我自己的个性,不一定代表正确,所以每个程序猿都应具有自己的想法!!!
注:常量由const修饰的放在内部FLASH,只要是变量就放在内部SRAM
注:DMA总线与Dcode总线会经过总线矩阵进行仲裁
拓展:
1-RAM随机存储器random access memory:存储变量-给定一个地址, 可以立即访问到数据(存取的速度与存储单元的位置无关)
SRAM:静态-主要用作高速缓冲存储器,连接使用方便(不需要刷新电路)、工作稳定、存取速度快(约为动态随机存取储存器DRAM的3~5倍)、使用简单
DRAM:动态-内存条,存取慢,便宜
2-ROM只读存储器Read-Only Memory:掉电数据不丢失-如EEPROM
3-FLASH闪存
/* GPIOB 端口全部输出高电平*/
//1-直接操作地址,将地址强制转换为指针,进而操作指针来达到操作寄存器的目的
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
//2-宏定义对寄存器指针封装
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR = 0xFF;
注:0x4001 0C0C 在我们看来是 GPIOB 端口 ODR 的地址,但是在编译器看来,这只是一个普通的变 量,是一个立即数
*要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即 (unsigned int )0x4001 0C0C,然后再对这个指针进行 * 操作
GPIO的寄存器以及对应功能和使用方法有必要记住
typedef struct {
//每个GPIO端口有两个32位 配置寄存器
uint32_t CRL;//GPIO 端口配置低寄存器
uint32_t CRH;//GPIO 端口配置高寄存器
//每个GPIO端口有两个32位 数据寄存器
uint32_t IDR;//GPIO 数据输入寄存器
uint32_t ODR;//GPIO 数据输出寄存器
uint32_t BSRR;//GPIO 位设置/清除寄存器
uint32_t BRR;//GPIO 端口位清除寄存器
uint16_t LCKR;//GPIO 端口配置锁定寄存器
} GPIO_TypeDef;
寄存器组 | 用途 | 简介 |
---|---|---|
CRL与CRH这一对配置寄存器 | 存放输入输出模式、输出速度配置 | CRL存放低8位:GPIOx(x=0~7)的配置 |
IDR与ODR这一对数据寄存器 | IDR是查看引脚点平状态用的,ODR是引脚电平输出的寄存器 | I:只读;D:可读可写 |
BSRR端口位设置/清除寄存器 |
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//读取存取在IDR寄存器中的引脚电平
//@retval 1(Bit_SET)或者 0(Bit_RESET)
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);//向端口输出寄存器写入引脚的电平状态
//@arg PortVal:操作某个端口的0-15这16个引脚的电平状态
寄存器和 ODR 寄存器具有类似的作用,都可以用来设置 GPIO 端口的输出位是 1 还是 0
端口位设置 | 端口位清除 |
---|---|
0~15这低16位 | 16~31这高16位 |
注:操作BRSS高16位效果等同操作BRR低16位(BRR高16位保留不使用),一般用BRR直接对位进行清除操作
//直接操作寄存器
GPIOA->BSRR=1<<1;//对低16位的位1(0~15)进行操作----->效果:使PIN1电平置高
GPIOA->BSRR=1<<(16+1);//对高16位的位1进行操作------>效果:使PIN1清除高电平(变为低电平)
//调用标准库的gpio.c的函数
GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);//函数中置高操作用的是BSRR,置低操作用的是BRR
//@arg BitVal:1(Bit_SET); 0(Bit_RESET)
注:对BRSS与BRR寄存器的操作效果可由标准库的GPIO_SetBits()和函数 GPIO_ResetBits()来完成(转3-2-1)
锁定机制允许冻结IO配置。当在一个端口位上执行了锁定(LOCK)程序,在下一次复位之前,将不能再更改端口位的配置
基于STM32F10x
相关性 | 桥接1 | 桥接2 |
---|---|---|
AHB系统总线 | APB2 | APB1 |
ADC1~ADC3 | DAC | |
GPIOA-GPIOG | I2C1、I2C2 | |
EXTI | IWDG、WWDG | |
AFIO | bxCAN | |
USART-> | USART1 | USART2、USART3 |
SPI-> | SPI1 | SPI3/I2S、SPI2/I2S |
TIM-> | 高级定时器TIM1、TIM8 | TIM2 ~ TIM7 |
UART-> | UART4、UART5 | |
RTC | ||
USB,PWR,BKP |
所有端口都有外部中断能力。为了使用外部中断线,端口必须配置成输入模式
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_AFIO,ENABLE);
STM32上有很多I/O口,也有很多的内置外设如:I2C,ADC,ISP,USART等 ,为了节省引出管脚,这些内置外设基本上是与I/O口共用管脚的,也就是I/O管脚的复用功能。但是STM32还有一特别之处就是:很多复用内置的外设的I/O引脚可以通过 重映射功能 , 从不同的I/O管脚引出,即复用功能的引脚是可通过程序改变的
注1:重映射ReMap解决GPIO引脚占用冲突
/*来自stm32f10x_gpio.c*/
//@arg GPIO_Remap:选择需要重映射的引脚Pin
//@arg NewState: ENABLE/DISABLE
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);//重映射函数
//调用函数进行重映射:使I/O口重映射开启
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);
//注:一般为GPIO_Remap_xxx,特殊的找GPIO的source文件
程序中用到的USART2外设的TX,RX分别对应PA2,PA3,但是我的学习板上的PA2,PA3引脚接了其他设备,但是为了还要用USART2,“RCC_APB2Periph_GPIOD |RCC_APB2Periph_AFIO”就打开了GPIOD重映射功能把USART2设备的TX,RX映射到PD5,PD6上
注2:重映射有要求,不可随意映射!!!
思考:电磁车比赛用的STM32F103C8T6引脚较自己的野火指南者(VE)学习版引脚少了不少,由于前期分配不正确导致许多外设不能按原引脚使用,给硬件猿造成麻烦(其实有些可以重映射),初学者配置引脚别被CubeMX这个软件误导了,要去看 数据手册的引脚定义 !!!!!
1、STM32中,USART2和TIM2是共用相同IO的,你如何决定这几个IO到底是做USART2还是做TIM2呢?如果你要同时使用USART2和TIM2,该怎么办?
2、不是说使用了IO的复用功能就一定要启动RCC_APB2Periph_AFIO的Clock只有使用了
AFIO的事件控制寄存器(AFIO_EVCR):配置EVENTOUT事件输出
复用重映射和调试I/O配置寄存器(AFIO_MAPR):配置复用功能重映射
外部中断配置寄存器x(AFIO_EXTICRx):配置外设中断线映射
才需要开启AFIO的时钟
查阅外设的重映射引脚在中文参考手册的P117-121
以重映射适用于多个不同引脚的封装的USART1举例
0=映射前,1=映射后;USART1仅能完全重映射
复用功能 | USART1_REMAP=0 | USART1_REMAP=1 |
---|---|---|
TX | pA9 | pB6 |
RX | pA10 | pB7 |
部分重映射:功能外设的部分引脚重新映射,还有一部分的引脚是原来的默认引脚
完全重映射:功能外设的所有引脚都重新映射
1-打开重映射AFIO时钟 和 USART1重映射后的I/O口的端口PORT的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO,ENABLE);
2-I/O口重映射开启
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);
3-.配置重映射后的引脚
注1:对于需要复用的输入功能,端口必须配置成输入模式(浮空、上拉或下拉)且输入引脚必须由外部驱动
注2:对于复用输出功能,端口必须配置成复用功能输出模式(推挽或开漏)
注3:对于双向复用功能,端口位必须配置复用功能输出模式(推挽或开漏)。这时,输入驱动器被 配置成浮空输入模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//USART_TX发射端
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;//USART_RX接收端
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOB, &GPIO_InitStructure);
typedef struct{
uint16_t GPIO_Pin; //引脚
GPIOSpeed_TypeDef GPIO_Speed; //输出速度,一般为50MHZ(输入模式不用配置)||对功耗有要求是须按需设置
GPIOMode_TypeDef GPIO_Mode; //模式(很重要,很容易弄混!)
}GPIO_InitTypeDef;
// 1(Bit_SET); 0(Bit_RESET)
GPIO_SetBits(GPIO_PORT,GPIO_PIN) //拉高引脚输出电平
GPIO_ResetBits(GPIO_PORT,GPIO_PIN) //拉低引脚输出电平
TIM配置看下边外设GPIO配置里的内容
//输入模式
- 浮空输入(GPIO_Mode_IN_FLOATING)//KEY,USART_RX,SPI_SCK(从),SPI_MOSI(从),SPI_MISO(主),BxCAN_RX
- 上拉输入(GPIO_Mode_IPU) //默认高电平,当下降沿触发EXTI外部中断
- 下拉输入(GPIO_Mode_IPD) //默认低电平,当上升沿触发EXTI外部中断
- 模拟输入(GPIO_Mode_AIN) //应用ADC模拟输入,或者低功耗下省电
//输出模式
- 开漏输出(GPIO_Mode_Out_OD) //
- 推挽输出(GPIO_Mode_Out_PP) //
- 复用开漏输出功能(GPIO_Mode_AF_OD)//I2C_SCL,I2C_SDA
- 复用推挽输出功能(GPIO_Mode_AF_PP)//USART_TX,SPI_SCK(主),SPI_MOSI(主),SPI_MISO(从),BxCAN_TX
1 易受干扰
2 芯片复位上电,默认就是浮空状态
浮空最大的特点就是电压的不确定性,它可能是0V,也可能是VCC,还可能是介于两者之间的某个值。多用于外部按键,这样可以减少上下拉电阻对结果的影响
1 有效防干扰
2 用于EXTI外部中断,易于检测与默认设置电平相反的外部输入信号(易于检测上升沿或下降沿这两种脉冲信号)
例1:电磁小车寻迹完成后,需要在布有磁铁的黑色斑马线停下,需要检测片外外设——干簧管的电平变化。通常设置正常跑车——没有外部信号传入,默认将干簧管下拉为低电平,当经过磁铁时,干簧管导通,单片只因接收到高电平——上升沿触发外部中断,执行停车命令
例2:外部按键KEY——设置为外部输入信号为高电平时,即上升沿触发按键的外部中断去执行用户设定的操作。没有按下时默认电平为低电平,需要设置为下拉输入(可以设置为浮空输入)
采集来自外部的0~Vss(即STM32芯片电压3.3V)的电压值
注:正常情况高电平时没有驱动能力(即无法输出高电平)
解决方法:给予需要开漏输出的外部电路一个上拉电阻,这样的输出的高电平时Vcc的电压,是完全由用户决定的电压,实现了_电平转换_
适用于多个信号线直接连接在一起
只有当所有信号全部为高电平时,合在一起的总线为高电平;
只要有任意一个或者多个信号为低电平,则总线为低电平
1、提高引脚的驱动能力
2、提高电路的负载能力,例如音频放大器或电机驱动器等
3、提高开关速度
注1:开漏输出只连接Vss(0V),而推挽输出通过控制选择连接了VDD(3.3V)或Vss(0V)进行直接输出
注2:开漏具有‘线与’特性,而推挽没有(如果高电平和低电平连在一起,会出现电流倒灌,损坏器件)
为片内外设而生的专用输出模式
中文参考手册p109
片内、外设是两个概念,片内指做成芯片的集成电路内部,简称**片内,**片外同理显而易见;外设是外部设备的简称,是指集成电路芯片外部的设备。现在许多芯片在制造时已经能够将部分接口电路和总线集成到芯片内部
一些芯片必备的外设,如ADC、I2C、USART等
满足用户需求的外部设备,如外部按键KEY等
TIM1/TIM8引脚 | 功能配置 | GPIO配置 |
---|---|---|
CHx | 输入捕获通道x | 浮空输入 |
CHx | 输出比较通道x | 推挽复用输出 |
CHxN | 输出比较通道x | 推挽复用输出 |
BKIN | 刹车输入 | 浮空输入 |
ETR | 外部触发时钟输入 | 浮空输入 |
TIM2/3/4/5引脚 | 功能配置 | GPIO配置 |
---|---|---|
CHx | 输入捕获通道x | 浮空输入 |
CHx | 输出比较通道x | 推挽复用输出 |
ETR | 外部触发时钟输入 | 浮空输入 |
基本定时器只具备最基本的定时功能,就是累加的时钟脉冲数超过预定值时,能触发中断或触发DMA请求
USART引脚 | 功能配置 | GPIO配置 |
---|---|---|
TX | 全双工、半双工同步模式 | 推挽复用输出 |
RX | 全双工模式 | 浮空输入或带上拉输入 |
RX | 半双工同步模式 | 未用,可作为通用I/O |
CK | 同步模式 | 推挽复用输出 |
RTS | 硬件流量控制 | 推挽复用输出 |
CTS | 硬件流量控制 | 浮空输入或带上拉输入 |
SPI引脚 | 功能配置 | GPIO配置 |
---|---|---|
SCK | 主模式 | 推挽复用输出 |
SCK | 从模式 | 浮空输入 |
MOSI | 全双工模式/主模式 | 推挽复用输出 |
MOSI | 全双工模式/从模式 | 浮空输入或带上拉输入 |
MOSI | 简单的双向数据线/主模式 | 推挽复用输出 |
MOSI | 简单的双向数据线/主模式 | 未用,可作为通用I/O |
MISO | 全双工模式/主模式 | 浮空输入或带上拉输入 |
MISO | 全双工模式/从模式 | 推挽复用输出 |
MISO | 简单的双向数据线/主模式 | 未用,可作为通用I/O |
MISO | 简单的双向数据线/从模式 | 推挽复用输出 |
NSS | 硬件主/从模式 | 浮空输入或带上拉输入或带下拉输入 |
NSS | 硬件主模式/NSS输出使能 | 推挽复用输出 |
NSS | 软件模式 | 未用,可作为通用I/O |
I2S引脚 | 功能配置 | GPIO配置 |
---|---|---|
WS | 主模式 | 推挽复用输出 |
WS | 从模式 | 浮空输入 |
CK | 主模式 | 推挽复用输出 |
CK | 从模式 | 浮空输入 |
SD | 发送器 | 推挽复用输出 |
SD | 接收器 | 浮空输入或带上拉输入或带下拉输入 |
MCK | 主模式 | 推挽复用输出 |
MCK | 从模式 | 未用,可作为通用I/O |
I2C引脚 | 功能配置 | GPIO配置 |
---|---|---|
SCL | 时钟线 | 开漏复用输出 |
SDA | 数据线 | 开漏复用输出 |
BxCAN引脚 | 功能配置 | GPIO配置 |
---|---|---|
CAN_TX | 发送端 | 推挽复用输出 |
CAN_RX | 接收端 | 浮空输入或带上拉输入 |
1、STM32配置的I/O引脚的输出速度不是输出信号的速度,而是I/O口驱动电路的响应速度(即信号变换的速度——如:高电平转化为低电平的速度)
2、频率越高,电平改变的折线的越陡,电平改变的就越快
3、频率越高,功耗越高;例如:点亮一个LED灯没必要上50MHZ的输出速度,降低输出速度达到节能的效果
但是通信时,对引脚高低电平的变换要求高
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz ;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
三种不同的时钟源可被用来驱动系统时钟(SysCLK):
● HSE振荡器时钟
● HSI振荡器时钟
● PLL时钟
1、设置系统时钟 SYSCLK
2、设置 AHB 分频因子(决定 HCLK 等于多少)
3、设置 APB2 分频因子(决定 PCLK2 等于多少)
4、设置 APB1 分频因子(决定 PCLK1 等于多少)
5、设置各个外设的分频因子; 控制 AHB、APB2 和 APB1 这三条总线时钟的开启、控制每个外设的时钟的开启
注:时钟的一般配置为PCLK2 = HCLK = SYSCLK = PLLCLK = 72M;PCLK1 = HCLK / 2 = 36M(最高)
(高速时钟桥接线APB2设置为72MHZ,低速时钟桥接线APB1设置为36MHZ)
时钟树的图丢失时见中文参考手册P56
HSE 最常使用的就是8M的无源晶振
//HSE作为时钟来源,经过PLL倍频作为系统时钟SYSCLK
void HSE_SetSysCLK(uint32_t RCC_PLLMul_x);//x为倍频因子
//注:PLLMul常为9:即8MHZ * 9(倍频) = 72MHZ(SYSCLK)
主要缺点是不稳定
频率为8M,根据温度和环境的情况频率会有漂移,一般不作为 PLL 的时钟来源,是HSE无法正常开启的无奈之选
PLL 时钟来源可以有两个,一个来自 HSE,另外一个是 HSI / 2
注:如果HSI要作为PLL时钟的来源的话,必须二分频之后才可以,即HSI / 2,而PLL倍频因子最大只能是16;所以当使用HSI的时候,SYSCLK最大只能是4MHZ x 16=64MHZ
经过HSE或HSI这两个时钟源倍频后的产物
系统时钟来源可以是:HSI、PLLCLK、HSE
一般设置为SYSCLK = PLLCLK = 72M(将PLLCLK切换成SYSCLK)
AHB系统总线(总线时钟 HCLK)
|————————>外设高速时钟线APB2(总线时钟PCLK2)
|
|————————>外设低速时钟线APB1(总线时钟PCLK1)
注:挂载在这些线上的外设详看3-1-1
系统时钟 SYSCLK 经过 AHB 预分频器分频之后得到时钟叫 APB 总线时钟,即 HCLK
APB2 总线时钟 PCLK2 由 HCLK 经过高速 APB2 预分频器得到
APB1 总线时钟 PCLK1 由 HCLK 经过低速 APB 预分频器得到
void HSE_SetSysCLK(uint32_t RCC_PLLMul_x)//配置系统时钟
//做准备工作
RCC_DeInit();// 重置RCC时钟配置为默认复位状态,这句是必须的
1、开启(使能)HSE ,并等待 HSE 稳定
RCC_HSEConfig(RCC_HSE_ON);// 使能HSE,开启外部晶振,野火开发板用的是8M
/* 判断HSE是否就绪 */
HSEStartUpStatus=RCC_WaitForHSEStartUp();//返回成功/失败
if(HSEStartUpStatus==SUCCESS)//HSE启动成功
//在HSE启动成功的条件下执行操作FLASH的准备操作
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);// 使能FLASH预存取缓冲区(使能预取指)
FLASH_SetLatency(FLASH_Latency_2);// 设置为两个等待
2、设置 AHB、APB2、APB1的预分频因子
RCC_HCLKConfig(RCC_HCLK_Div1);// HCLK = SYSCLK = 72MHZ
RCC_PCLK2Config(RCC_HCLK_Div1);// PCLK2 = HCLK = 72MHZ
RCC_PCLK1Config(RCC_HCLK_Div2);// PCLK1 = HCLK/2 = 36MHZ
3、设置PLL的时钟来源,和PLL的倍频因子,设置各种频率主要就是在这里设置
/* 配置锁相环:
1-配置锁相环的时钟来源的分频因子:HSE_Div1(HSE不分频为8MHZ);
2-倍频因子PLLCLK = HSE * 9 = 72 MHz;*/
RCC_PLLConfig(RCC_PLLSource_HSE_Div1,RCC_PLLMul_x);
4、开启PLL,并等待PLL稳定
RCC_PLLCmd(ENABLE);//使能锁相环PLL
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY)==RESET);//如果PLL的状态处于重置RESET就循环,SET准备就绪跳出循环
5、把PLLCLK切换为系统时钟SYSCLK
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);//配置PLL锁相环作为系统时钟SYSCLK的source来源
6、读取时钟切换状态位,确保PLLCLK被选为系统时钟
//等待PLL锁相环切换为系统时钟
while(RCC_GetSYSCLKSource()!=0x08);//没成功设置为系统时钟就一直循环
//对于HSE启动失败的补救措施,即在:if(HSEStartUpStatus!=SUCCESS)条件下
else{//HSE没成功启动用户可以在下里添加处理错误的代码
HSI_SetSysCLK(uint32_t RCC_PLLMul_x);//可以换用HSI作为时钟源
}
void HSI_SetSysCLK(uint32_t RCC_PLLMul_x);//略...
ADC1~ADC3都挂载在APB2(系统时钟PCLK2)上
ADC 时钟由 PCLK2 经过 ADC 预分频器【2/4/6/8分频】得到
ADC 时钟理论最高只能是14M,但是一般设置PCLK=72MHZ,则实际最大为12MHZ(6分频:72M(PCLK2) / 6 = 12M)
ADC 的转换时间跟 ADC 的采样时间和输入时钟有关:
T c o n v = 采样时间 + 12.5 个周期 Tconv = 采样时间 + 12.5 个周期 Tconv=采样时间+12.5个周期
T = 1 / ADC_CLK = 1 / 12 us
采样时间最短为1.5个周期,则转换时间Tconv = 14个ADC周期 = 14 / 12 us =1.17us(常用转换时间)
USB 的时钟最高是 48M,根据分频因子反推过来算,PLLCLK 只能是 48M 或者是 72M。一般我们设置 PLLCLK=72M, USBCLK=48M
注:USB对时钟要求比较高,所以 PLLCLK 只能是由 HSE 倍频得到,不能使用 HSI 倍频
系统时钟由 HCLK 8分频得到,等于 9M,Cortex 系统时钟用来驱动内核的系统滴答定时器SysTick
RTC 时钟可由 HSE/128 分频得到,也可由低速外部时钟信号 LSE 提供,频率为 32.768KHZ,也可由低速内部时钟信号 LSI 提供
独立看门狗的时钟只能由LSI提供,LSI是低速的内部时钟信号,频率为 30~60KHZ 直接不等,一般取 40KHZ
MCO 的时钟来源可以是:PLLCLK/2、HSI、HSE、SYSCLK
1、引脚由 PA8 复用所得,主要作用是可以对外提供时钟,相当于一个有源晶振
2、通过示波器监控 MCO 引脚的时钟输出来验证系统时钟配置是否正确
当 HSE 故障的时候,如果 PLL 的时钟来源是 HSE,那么当 HSE 故障的时候,不仅 HSE 不能使用,连 PLL 也会被关闭,这个时候系统会自动切换 HSI 作为系统时钟,此时 SYSCLK=HSI=8M, 如果没有开启 CSS 和 CSS 中断的话,那么整个系统就只能在低速率运行,这是系统跟瘫痪没什么两样。如果开启了 CSS 功能的话,那么可以当 HSE 故障时,在 CSS 中断里面采取补救措施, 使用 HSI,并把系统时钟设置为更高的频率,最高是 64M,64M 的频率足够一般的外设使用,如: ADC、SPI、I2C 等
HSI做PLLCLK的时钟源,必须Div2二分频
//第1/2/4/5步与HSE的配置完全一样
/* 第三步:配置锁相环时钟PLLCLK:
1-配置锁相环的时钟来源的分频因子:HSI(必须要二分频);
2-倍频因子PLLCLK = (8MHZ / 2) * PLLMul */
RCC_PLLConfig(RCC_PLLSource_HSI_Div2,RCC_PLLMul_x);
注1: 每个外设都可以产生中断
注2:异常就是中断,中断就是异常
中断的核心 片上外设
NVIC中断库函数查看misc.c
PreemptionPriority:(抢占优先级)主优先级
SubPriority : 子优先级
=============================================================================================================================
NVIC_PriorityGroup | NVIC_IRQChannelPreemptionPriority | NVIC_IRQChannelSubPriority | Description
=============================================================================================================================
NVIC_PriorityGroup_0 | 0 | 0-15 | 0 bits for pre-emption priority
| | | 4 bits for subpriority
-----------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_1 | 0-1 | 0-7 | 1 bits for pre-emption priority
| | | 3 bits for subpriority
------------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_2 | 0-3 | 0-3 | 2 bits for pre-emption priority
| | | 2 bits for subpriority
------------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_3 | 0-7 | 0-1 | 3 bits for pre-emption priority
| | | 1 bits for subpriority
------------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_4 | 0-15 | 0 | 4 bits for pre-emption priority
| | | 0 bits for subpriority
=============================================================================================================================
typedef struct {
uint8_t NVIC_IRQChannel; // 中断源
uint8_t NVIC_IRQChannelPreemptionPriority; // 抢占优先级
uint8_t NVIC_IRQChannelSubPriority; // 子优先级
FunctionalState NVIC_IRQChannelCmd; // 中断使能或者失能
} NVIC_InitTypeDef;
小心:中断源别搞错了,即使写错了程序也不会报错,只会导致不响应中断
typedef enum IRQn{
//详见stm32f10x.h,枚举好了片上外设
} IRQn_Type;
注:像外部按键KEY这种片外外设.h文件只是给出了EXTIx_IRQn(x = EXTI_ Line x,即中断源对应相应的中断/事件线)
统一写在 stm32f10x_it.c 这个库文件中
每个中断/事件都有独立的触发和屏蔽 每个中断线都有专用的状态位
产生中断
产生事件
这两个功能从硬件上就有所不同
框图丢失时查看中文参考手册P135或野火库开发教程P259
注1:图中20代表 在控制器内部类似的信号线路有 20 个这与 EXTI 总共有 20 个中断/事件线是吻合的
注2:EXTI挂载在APB2总线上!!!
注:红色虚线指示的电路流程。它是一个产生中断的线路,最终信号流入到 NVIC 控制器内
绿色虚线:它是一个产生事件的线路,最终输出一个脉冲信号(这个脉冲信号可以给其他外设电路使用,比如定时器 TIM、模拟数字转换器ADC等等,这样的脉冲信号一般用来触发 TIM 或者 ADC 开始转换)
红色虚线:产生中断线路目的是把输入信号输入到 NVIC,进一步会运行中断服务函数,实现功能
枚举体查看stm32f10x.h
Line | 输入源 |
---|---|
EXTI 0-EXTI 15 | pX 0-pX 15(X=A~I) |
EXTI 16 | PVD输出 |
EXTI 17 | RTC闹钟事件 |
EXTI 18 | USB唤醒事件 |
EXTI 19 | 以太网唤醒事件(互联型) |
枚举体查看stm32f10x.h
中断源 | |
---|---|
EXTI0_IRQn ~ EXTI4_IRQn | |
EXTI9_5_IRQn | |
EXTI15_10_IRQn |
typedef struct{
uint32_t EXTI_Line; // 中断/事件线
EXTIMode_TypeDef EXTI_Mode; // EXTI 模式:Interrupt产生中断模式,Event产生事件模式
EXTITrigger_TypeDef EXTI_Trigger; // 触发类型:上升/下降沿触发,上或下都触发
FunctionalState EXTI_LineCmd; // EXTI 使能
} EXTI_InitTypeDef;
以外部按键KEY的按下中断服务函数为例
电路图 ------按键------
GPIO引脚pXx—————— ——————3.3V
思考:KEY按下是不是仅有上升沿才可以触发中断服务
按下包括三个过程:按下按键未与左右两边接触->与两边接触->松开按键
GPIO的电平也是三个状态:浮空->3.3V->浮空(或GND)
在这之间有上升沿和下降沿,都代表了按键按下这个操作,需要执行中断服务函数
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; /* 配置为浮空输入 */
配置 NVIC
初始化 EXTI
//下面两种均可,按下即保持亮灯
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;//按下检测到按键
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//松手检测到按键
//这种是按住才执行:亮灯
EXTI_InitStructure.EXTI_Trigger =EXTI_Trigger_Rising_Falling;//很神奇吧
思考:下次可以给小车搞个上或下降沿都触发的按键,在持续按下这个按键的时候小车电机不转,松开就开始发车
可以统一放在stm32f10x_it.c进行集成化管理,且不需要在它的.h声明该函数
void KEY1_IRQHandler(void)
{
if(EXTI_GetITStatus(KEY1_INT_EXTI_LINE) != RESET)//确保是否产生了EXTI Line中断
{
LED1_TOGGLE;// LED1 取反
EXTI_ClearITPendingBit(KEY1_INT_EXTI_LINE);//清除中断标志位(很必要!!!)
}
}
定时器 | SysTick | TIM6与TIM7 |
---|---|---|
核心区别 | 内核外设 | 片上外设 |
移植性 | 所有的Cortex-M内核单片机都有且一致 | 各厂家百花齐放 |
特征 | 功能简单,只能递减 | 功能多,配置相对复杂 |
硬件优先级 | (属于系统内核)更高 | 更低 |
抗干扰性 | 强(除非系统异常,比如复位,否则滴答定时器将稳定运行) | 容易受外界干扰 |
面向对象 | 一般用于操作系统时间,进程切换等 | 用于用户需求 |
SysTick—系统定时器是属于 CM3 内核中的一个外设,内嵌在 NVIC 中
SysTick像是维持操作系统的心跳
#include "core_cm3.h"//SysTick 属于内核的外设,有关的寄存器定义和库函数都在内核相关的库文件core_cm3.h
#include "misc.h"//
WARMING:上面这两个文件顺序错了会保错!!!
内核优先级一定比外设优先级高
中断优先级的分组对 内核外设 和 片上外设 同样适用(即不存在内核优先级一定比外设优先级高的说法)
由软件划分的优先级
总体配置 | (主)抢占优先级 | 子优先级 |
---|---|---|
SysTick:(DEC)15;(BIN)1111 | (DEC)3;(BIN)11 | (DEC)3;(BIN)11 |
EXTI:(DEC)3;(BIN)1111 | (DEC)0;(BIN)00 | (DEC)3;(BIN)11 |
结论:SysTick中断优先级远小于EXTI(总体配置的十进制DEC数字越小中断优先级越高)
特例:由硬件划分的优先级来看
硬件优先级查看中文手册p132
当两者的抢占优先级与子优先级配置都相同时,向量表中显示:SysTick高于EXTI
结论:中断向量表中的硬件编号,编号越小,优先级越高
void SysTick_Init(void)
{/* SystemCoreClock / 1000 1ms 中断一次
* SystemCoreClock / 100000 10us中断一次
* SystemCoreClock / 1000000 1us 中断一次*/
if (SysTick_Config(SystemCoreClock / 100000)) // ST3.5.0库版本
{
/* Capture error */
while (1);
}
}
SysTick_Config()的形参 ticks 用来设置重装载寄存器的值,最大不能超过重装载寄存器的值 224,当重装载寄存器的值递减到 0 的时候产生中断, 然后重装载寄存器的值又重新装载往下递减计数
时钟源:AHBCLK=72M,将AHBCLK这个时钟源转化为SysTickCLK
SystemCoreClock = SYSCLK_FREQ_72MHz = 72000000
t = T i c k s ∗ 1 / f = ( 72000000 / 100000 ) ∗ ( 1 / 72000000 ) = 10 u s t = Ticks * 1/f = (72000000/100000) * (1/72000000) = 10us t=Ticks∗1/f=(72000000/100000)∗(1/72000000)=10us
t : 定时时间(延时单位);Ticks : 多少个时钟周期产生一次中断;f : 时钟频率 72000000(72MHZ)
void SysTick_Delay_MS( __IO uint32_t ms)
{
uint32_t i;
SysTick_Config(SystemCoreClock/1000);
for(i=0;i<ms;i++)
{
// 当计数器的值减小到0的时候,CRTL寄存器的位16会置1
// 当置1时,读取该位会清0
while( !((SysTick->CTRL)&(1<<16)) );
}
// 关闭SysTick定时器
SysTick->CTRL &=~ SysTick_CTRL_ENABLE_Msk;
}
//SysTick_Delay_MS与SysTick_Delay_US
SysTick_Config(SystemCoreClock/1000);//中断(延时)单位为 1ms
SysTick_Config(SystemCoreClock/1000000);//中断(延时)单位为 1us
#define SysTick_DELAY_1S SysTick_Delay_MS(1000)
SysTick_DELAY_1S;//使用宏定义相当于 SysTick_Delay_MS(1000); 达到延时1秒的作用
纯纯只有一个定时的作用
基本定时器的核心是时基
定时器时钟 TIMxCLK,即内部时钟 CK_INT,经 APB1预分频器(TIM2 ~ TIM7)后分频提供
typedef struct
{//时基结构体
uint16_t TIM_Prescaler; //预分频器 时钟源经该预分频器才是定时器计数时钟 CK_CNT
uint16_t TIM_CounterMode; //计数模式 向上计数、向下计数以及中心对齐
uint16_t TIM_Period; //定时器周期 配置ARR(0 至 65535)
uint16_t TIM_ClockDivision; //时钟分频
//设置定时器时钟 CK_INT 频率与死区发生器以及数字滤波器采样时钟频率分频比
uint8_t TIM_RepetitionCounter; //重复计算器 只有 8 位,只存在于高级定时器
} TIM_TimeBaseInitTypeDef
预分频器(PSC)
定时器时钟 TIMxCLK(即CK_INT) 经过 PSC预分频器之后,即 CK_CNT,用来驱动计数器计数
注:定时器的时钟源为内部时钟,频率高,则定时间隔短(不符合实际用途,也不便于计算),需要PSC预分频器来降低频率,让计数器使用降频后的 CK_CNT
例子:为何分频后便于计算?
如果我们想获取一个精确的1ms中断,如果不分频,72MHz的时钟对应每周期1/72us,十分不利于计算。这时候使用预分频器将其72分频后为1MHz,每周期1us,1000个计时周期即为1ms,这样既便于计算,定时也更加精确
为何预分频计数器 Prescaler 的值为(目标值 - 1)
如下面工作原理介绍:达到PSC设定值后**‘‘再tick一次后计数器归零’’**,定时器时钟 CK_CNT 频率 CK_INT / (PSC + 1)
- 想对时钟源进行72分频,那么预分频器的值就应该设置为71
PSC 工作原理
定时器时钟源每tick一次,预分频器计数器值+1,直到达到预分频器的设定值,然后再tick一次后计数器归零,同时,CNT计数器值+1
- 举个形象的比喻
预分频计数器PSC相当于分针,CNT计数器相当于时针,假定PSC为(60 - 1)
CK_INT tick一次,分钟+1(相当于PSC++),加到59min的时候,达到PSC设定的值,再Ticks一次达到60min,时钟+1(相当于CNT++)
注:预分频器值寄存器TIMx_PSC存在影子寄存器(官方翻译为缓冲功能),所以在定时器启动后更改TIMx_PSC的值并不会立即影响当前定时器的时钟频率。要等到下一个更新事件(UEV)发生时才会生效
计数器(CNT)
16位的计数器,只往上计数,最大计数值为65535(当计数达到自动重装 载寄存器的时候产生更新事件,并清零从头开始计数)
自动重装载寄存器(ARR)
CNT++(即CNT向上计数)到ARR时,CNT清零
定时时间的计算
t 定时 = T 中断 ∗ N 中断 t定时 = T中断 * N中断 t定时=T中断∗N中断
计数器在 CK_CNT 的驱动下,计一个数的时间则是 CK_CNT 的倒数,即T = 1 /( CK_INT / (PSC+1) )
产生一次中断的时间:T中断 = (ARR + 1) / CK_CNT:即一个自动重装载周期的时间
果在中断服务程序里面设置一个变量 time(即需要中断的次数),那么就可以计算出我们需要的定时时间等于:t定时 = T中断 * time
1.中断优先级的配置
//NVIC的配置...略
相对于SysTick来说 浪费了 一个优先级
2.开启定时器时钟,即内部时钟CK_INT=72M
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6);//RCC_APB1Periph_TIM6为宏定义,是标准库封装好的
注:挂载在 APB2的外设 不能 用 APB1 的RCC_APB1PeriphClockCmd时钟使能函数
3.基本定时器Timer的3项时基的配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;//结构体声明
TIM_TimeBaseStructure.TIM_Prescaler= (72 - 1);// 时钟源 CK_INT 被分频为 CK_CNT = 1MHZ >>>>>T = 1us
TIM_TimeBaseStructure.TIM_Period = (1000 - 1); // 自动重装载寄存器的值ARR >>>>>T = 1us * 1K = 1ms
//默认向上计数,无需配置
4.初始化操作
TIM_TimeBaseInit(BASIC_TIM, &TIM_TimeBaseStructure);// 初始化定时器
TIM_ClearFlag(BASIC_TIM, TIM_FLAG_Update);// 清除计数器中断标志位
TIM_ITConfig(BASIC_TIM,TIM_IT_Update,ENABLE);// 开启计数器中断
TIM_Cmd(BASIC_TIM, ENABLE);// 使能计数器
通信接口 | 全称 | 简述特性 |
---|---|---|
UART | 通用异步接收发送器 | 全双工异步串行通信 |
USART | 通用同步异步接收发送器 | 全双工同步或异步串行通信 |
I2C | 集成电路总线 | 半双工同步串行通信 |
SPI | 串行外设接口 | 高速的全/半双工同步串行通信 |
注: USART还支持同步模式,因此USART 需要同步始终信号USART_CK(如STM32 单片机),通常情况同步信号很少使用,因此一般的单片机UART和USART使用方式是一样的,都使用异步模式
串口通讯结构图消失时查看野火库开发教程p295
在串口通讯的协议层中,规定了数据包的内容(它由启始位、主体数据、校验位以及停止位组成),通讯双方的数据包格式要约定一致才能正常收发数据
串口数据包的基本组成图消失时查看野火库开发教程p300
UART 与 USART(异步下) 是 串口异步通讯 ,异步通讯中由于没有时钟信号,两个通讯设备之间需要事先约定好波特率,即每个码元(用虚线分开的每一格就是代表一个码元)的长度
校验方法 | 简述 | 例子 |
---|---|---|
奇校验 | 有效数据与校验位中“1”的个数为奇数 | 有效01101001,尾跟一个‘1’的校验位 |
偶校验 | 有效数据与校验位中“1”的个数为偶数 | 数据帧11001010,尾跟一个’0’的校验位 |
0校验 | 不管有效数据,校验位总为“0” | 置位逻辑低(0)校验 |
1校验 | 不管有效数据,校验位总为“1” | 置位逻辑高(1)检验 |
无校验 |
typedef struct
{
uint32_t USART_BaudRate; //波特率
uint16_t USART_WordLength; //字长
uint16_t USART_StopBits; //停止位
uint16_t USART_Parity; //校验位模式
uint16_t USART_Mode; //USART的模式
uint16_t USART_HardwareFlowControl; //硬件流的控制
} USART_InitTypeDef;
控制的结构体成员 | 简介 |
---|---|
波特率设置 | 2400、9600、19200、115200 |
数据帧字长 | 可选 8 位或 9 位(使能了奇偶校验) |
停止位设置 | 可选 0.5 个、1 个、1.5 个和 2 个停止位 |
校验模式选择 | |
USART 模式选择 | USART_Mode_Rx 和 USART_Mode_Tx |
硬件流控制选择 | 只有在硬件流控制模式才有效 |
以USART3为例
1.NVIC配置
//NVIC优先级分组
2.使能时钟与串口
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//GPIOB 挂载在APB2上
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);//USART3 挂载在APB1上
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);// 使能 串口接收中断
USART_Cmd(USART3, ENABLE);// 使能 串口
注:挂载在不同时钟线上的外设需要单独配置时钟使能!!!
串口中断模式选择
USART_IT | 指定启用或禁用USART中断源 |
---|---|
USART_IT_CTS | CTS更改中断(不适用于UART4和UART5) |
USART_IT_LBD | LIN断路检测中断 |
USART_IT_TXE | 发送数据寄存器空时中断 |
USART_IT_TC | 发送完成后中断 |
USART_IT_RXNE | 接收数据寄存器不空中断 |
USART_IT_IDLE | 空闲线路检测中断 |
USART_IT_PE | 奇偶校验错误中断 |
USART_IT_ERR | 中断错误(帧错误、噪声错误、超限错误) |
CTS与RTS 串口流控
在两个设备正常通信时,由于处理速度不同,在某些情况下,就可能导致丢失数据的情况
通过流控可以实现:当接收端数据处理不过来时,就发出“不再接收”的信号,发送端就停止发送,直到收到“可以继续发送”的信号再发送数据
RTS (发送请求)为输出信号,指示本设备准备好可接收数据,低电平有效,低电平说明本设备可以接收数据
CTS (发送允许)为输入信号,用于判断是否可以向对方发送数据,低电平有效,低电平说明本设备可以向对方发送数据
物理连接(交叉连接)
§ 主机的RTS(输出)信号,连接到从机的CTS(输入)信号。
§ 主机的CTS(输入)信号,连接到从机的RTS(输出)信号。
3.配置GPIO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//TX
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//RX
4.USART初始化结构体配置
USART_InitStructure.USART_BaudRate = 115200;// 配置波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;// 配置 帧数据字长 为8位
USART_InitStructure.USART_StopBits = USART_StopBits_1;// 配置停止位 为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);// 完成串口的初始化配置
5.发送操作
/***************** 发送一个字符 **********************/
void USATR_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
{
USART_SendData(pUSARTx,ch);/* 发送一个字节数据到USART */
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); /* 等待发送数据寄存器为空 */
}
/***************** 发送字符串 **********************/
void USATR_SendString( USART_TypeDef * pUSARTx, char *str)
{
unsigned int k=0;
do{
Usart_SendByte( pUSARTx, *(str + k) );
k++;
} while(*(str + k)!='\0');
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC) == RESET); /* 等待发送完成 */
}
6.重定向
#include //注:在USART_Config.h中需要包含
//重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
USART_SendData(USART3, (uint8_t) ch);/* 发送一个字节数据到串口 */
while (USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET); /* 等待发送完毕 */
return (ch);
}
//重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{
while (USART_GetFlagStatus(USART3, USART_FLAG_RXNE) == RESET); /* 等待串口输入数据 */
return (int)USART_ReceiveData(USART3);
}
7.串口中断服务函数(放在stm32f10x_it.c)
void DEBUG_USART_IRQHandler(void)
{
uint8_t ucTemp;
if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
{
ucTemp = USART_ReceiveData(DEBUG_USARTx);
USART_SendData(DEBUG_USARTx,ucTemp);
}
}
IIC总线物理拓扑图
只使用两条串行总线线路:数据线即用来表示数据,时钟线用于数据收发同步
每个连接到总线的设备都有一个独立的地址,来确保不同设备之间访问的准确性
高阻态性
总线通过上拉电阻接到电源,当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平
多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线
为了方便把IIC设备分为主设备和从设备,基本上谁控制时钟线(即控制SCL的电平高低变换)谁就是主设备
设备 | 功能 |
---|---|
主设备 | 主要产生时钟,产生起始信号和停止信号 |
从设备 | 可编程的IIC地址检测,停止位检测 |
半双工
同一时间单向通信
II总线C时序图
总线在传送数据过程中共有三种类型信号:开始信号、结束信号和应答信号
SDA与SCL空闲时
都被上拉置为高电平
信号类型 | 简介 |
---|---|
开始信号 | SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据 |
结束信号 | SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据 |
应答信号 | 接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据 |
注1:应答信号:CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障
注2:起始信号是必需的,结束信号和应答信号,都可以不要
起始信号
SCL线置高电平时,SDA线由高->低,本周期的通讯起始
停止信号
SCL线置高电平时,SDA线由低->高,本周期的通讯截止
SDA 数据线在 SCL 的 每个时钟周期传输一位 数据(1 bit)
传输时,SCL 为高电平的时候 SDA 表示的数据有效, 即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0
两个传输周期间:当 SCL 为低电平时,SDA 的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备
I2C 总线上的每个设备都有自己的独立地址
通过 SDA 信号线发送设备地址 (SLAVE_ADDRESS) 来查找从机
I2C 协议规定设备地址可以是 7 位 或 10 位
紧跟设备地址的一个数据位用来表示数据传输方向 ,它是数据方向位 (R/)
数据方向位 为“1”时表示 主机由从机 读数据,该位为“0”时表示 主机向从机 写数据
SDA数据线 在数据位R/W 操纵的情况
读数据:从机控制SDA,主机接收
写数据:主机控制SDA,从机接收
I2C 的数据和地址传输都带响应
响应包括“应答 (ACK)”和“非应答 (NACK)”两种信号
作为 数据接收端 时,当设备 (无论主从机) 接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答 (ACK)”信号,发送方会继续发送下一个数据
SDA控制权与应答情况
传输时主机产生时钟,在==第 9 个时钟(主机传输 8bit 后)【这是前提!!!】==时,数据发送端会释放 SDA 的控制权,由数据接收端控制 SDA,若 SDA 为高电平,表示非应答信号 (NACK),低电平表示应答信号 (ACK)
就是 ‘低应答’
共性:
时钟线置 高 时才能进行 起始或停止操作 或 数据传输有效
1.挂载两个总线的GPIO引脚初始化,开时钟,引脚电平初始化
//封装操纵 写入电平状态 的函数
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_15, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_5, (BitAction)(x))
//引脚电平初始化
OLED_W_SCL(1);//闲置为高阻态
OLED_W_SDA(1);//闲置为高阻态
2.IIC 开始
void OLED_I2C_Start(void)
{
OLED_W_SCL(1);//确保拉高(初始化)
OLED_W_SDA(1);//确保拉高(初始化)
OLED_W_SCL(0);//时钟给出:开始传输命令
OLED_W_SDA(0);//执行传输命令
}
3.IIC停止
void OLED_I2C_Stop(void)
{
OLED_W_SCL(1);//重返闲置的高阻态
OLED_W_SDA(0);//确保应答信号返回 或 无新的数据传输
OLED_W_SDA(1);//重返闲置的高阻态
}
注:
4.IIC发送 1bit
void OLED_I2C_SendByte(uint8_t Byte)
{
uint8_t i;
for(i = 0; i < 8; i++)//依次传入 8bit 数据
{
OLED_W_SDA(Byte & (0x80 >> i));
OLED_W_SCL(1);
OLED_W_SCL(0);
}
OLED_W_SCL(1); //额外的一个时钟,不处理应答信号
OLED_W_SCL(0);
}
5.OLED写命令
void OLED_WriteCommand(uint8_t Command)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x00); //写命令
OLED_I2C_SendByte(Command);
OLED_I2C_Stop();
}
6.OLED写数据
void OLED_WriteData(uint8_t Data)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x40); //写数据
OLED_I2C_SendByte(Data);
OLED_I2C_Stop();
}
7.写命令或者数据
void OLED_WR_Byte(uint8_t dat,uint8_t cmd)
{
if (cmd == OLED_Data)
{
OLED_WriteData(dat);
}
else if (cmd == OLED_Command)
{
OLED_WriteCommand(dat);
}
}
8.OLED_ShowChar(),OLED_Clear()
OLED_WR_Byte();//显示函数 大量使用到了这个 写命令/数据的函数
如何实现串行传输?
那就要用到数据移位寄存器
I2C 的 SDA 信号主要连接到数据移位寄存器上(数据由 并行形式 依次进入 数据移位寄存器 转化为 串行形式 在一位位发送出去)
数据移位寄存器的数据来源及目标是 数据寄存器 (DR) 、地址寄存器 (OAR)、PEC 寄存器以及 SDA 数据线
当从外部接收数据的时候,数据移位寄存器把 SDA 信号线采样到的数据一位一位地存储到“数据寄存器DR”中。 若使能数据校验,接收到的数据会经过 PCE 计算器运算,运算**结果存储在“PEC寄存器”**中
当 STM32 的 I2C 工作在从机模式的时候
接收到设备地址信号时,数据移位寄存器会把接收到的地址与 STM32 的自身的“I2C 地址寄存器”的值作比较,以便响应主机的寻址
STM32 的自身 I2C 地址可通过修改“自身地址寄存器”修改,支持同时使用两个 I2C 设备地址,两个地址分 别存储在 OAR1 和 OAR2 中
I2C 特性及架构
STM32 的 I2C 片上外设专门负责实现 I2C 通讯协议,只要配置好该外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来,CPU 只要检测 该外设的状态 和 访问数据寄存器 ,就能完成数据收发。这种由硬件外设处理 I2C 协议的方式减轻了 CPU 的工作,且使软件设计更加简单。
整体控制逻辑负责协调整个 I2C 外设
控制逻辑的工作模式:配置“控制寄存器 (CR1/CR2)”的参数
控制逻辑还根据要求,负责控制产生 I2C 中断信号、DMA 请求及各种 I2C 的通讯信号 (起始、停止、响应信号等)
在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器 (SR1 和 SR2)”,我们只要读取这些寄存器相关的寄存器位,就可以了解 I2C 的工作状态
I2C 架构图
理解工作原理
typedef struct {
uint32_t I2C_ClockSpeed; // 设置 SCL 时钟频率,此值要低于 400000
uint16_t I2C_Mode; // 指定工作模式,可选 I2C 模式及 SMBUS 模式
uint16_t I2C_DutyCycle; // 指定时钟占空比,可选 low/high = 2:1 及 16:9 模式
uint16_t I2C_OwnAddress1; // 指定自身的 I2C 设备地址
uint16_t I2C_Ack; // 使能或关闭响应 (一般都要使能)
uint16_t I2C_AcknowledgedAddress; // 指定地址的长度,可为 7 位及 10 位
} I2C_InitTypeDef;
掉电后数据不丢失的存储器,常用来存储一些配置信息,以便系统重新上电的 时候加载之
(1) 配置通讯使用的目标引脚为开漏模式;
(2) 使能 I2C 外设的时钟;
(3) 配置 I2C 外设的模式、地址、速率等参数并使能 I2C 外设;
(4) 编写基本 I2C 按字节收发的函数;
(5) 编写读写 EEPROM 存储内容的函数;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; //复用开漏输出
RCC_APB1PeriphClockCmd ( RCC_APB1Periph_I2C1, ENABLE ); //打开 I2C外设 的时钟
RCC_APB2PeriphClockCmd ( RCC_APB2Periph_GPIOB, ENABLE ); //打开 引脚GPIO 的时钟
static void I2C_Mode_Config(void)
{
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; /* I2C 配置 */
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//低 :高 = 2:1
I2C_InitStructure.I2C_OwnAddress1 = 0X0A; //与 STM32 外挂的 I2C 器件地址不一样即可
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ; //使能响应
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;/* I2C 的地址的长度 */
I2C_InitStructure.I2C_ClockSpeed = 400000; //400k快速模式 /* 通信速率 */
I2C_Init(I2C1, &I2C_InitStructure); /* I2C 初始化 */
I2C_Cmd(I2C1, ENABLE); /* 使能 I2C */
}
void I2C_EE_Init(void)
{
I2C_GPIO_Config();
I2C_Mode_Config();
/* 根据头文件 i2c_ee.h 中的定义来选择 EEPROM 要写入的设备地址 */
/* 选择 EEPROM Block0 来写入 */
EEPROM_ADDRESS = EEPROM_Block0_ADDRESS;
}
在大容量产品和互联型产品上,SPI接口可以配置为支持SPI协议或者支持I2S音频协议(小或中的不具有I2S)
串行外设接口(SPI)允许芯片与外部设备以 半/全双工、同步、串行方式通信。此接口可以被配置成主模式,并为外部从设备提供通信时钟(SCK)
可以使用一条双向数据线的双线单工同步传输,还可使用CRC校验的可靠通信
警告
由于 SPI3 / I2S3 的部分引脚与 JTAG 引脚共享 (SPI3_NSS/I2S3_WS 与 JTDI , SPI3_SCK/I2S3_CK与JTDO),因此这些引脚不受IO控制器控制,他们(在每次复位后) 被默认保留为JTAG用途。如果用户想把引脚配置给SPI3/I2S3,必须(在调试时)关闭 JTAG并切换至SWD接口,或者(在标准应用时)同时关闭JTAG和SWD接口。JTAG/SWD复用功能重映射详见:中文参考手册p117
SPI 通讯系统
片选线如图中是SS1,SS2与SS3
每个从设备都有独立的 这一条 NSS 信号线,本信号线独占主机的一个引脚
有多少个从设备,就有多少条片选信号线
I2C与SPI在从设备地址判定上的区别
通信协议 区别 I2C 通过设备地址来寻址、选中总线上的某个设备并与其进行通讯 SPI 没有设备地址,它使用 NSS 信号线来寻址,低电平选中(即片选有效) 注: SPI 通讯以NSS线置低电平 为 开始信号,以NSS线被拉高 作为 结束信号
STM32 的 SPI 时钟频率最大为 fpclk / 2,两个设备之间通讯时,通讯速率受限于低速设备
线 | 英 | 中 | 数据传输方向 |
---|---|---|---|
MOIS | Master Output,Slave Input | 主设备输出/从设备输入引脚 | 主->从 |
MISO | Master Input,,Slave Output | 主设备输入/从设备输出引脚 | 从->主 |
SPI通讯时序图
与I2C不同的是,不是SCL时钟线由高置低,而是NSS 信号线由高变低,是 SPI 通讯的起始信号
NSS 是每个从机各自独占的信号线,当从机在自己的 NSS 线检测到起始信号后,就知道自己被主机选中了
NSS 信号由低变高,是 SPI 通讯的停止信号
用的是 SCK时钟线 进行数据同步
MOSI 及 MISO 的数据在 SCK 的上升沿期间变化输出
在 SCK 的下降沿时被采样
SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制
注:SPI 一共有四种通讯模式
主要区别是总线空闲时 SCK 的时钟状态以及数据采样时刻
两个概念 | 简述 |
---|---|
CPOL时钟极性 | SPI空闲时,SCK 信号线的电平信号 (即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态) |
CPHA时钟相位 | 数据的采样时刻 |
输出PWM波的原理就是 从0开始计数到CCR 这个期间随计数增加不断置高电平,从CCR到自动重装载值Period期间不断置低电平
差异
属性 | 名 | 计数器类型 | 捕获/比较通道 | 互补输出 |
---|---|---|---|---|
高级 | TIM1,TIM8 | 向上/向下 | 4 | 有 |
通用 | TIM2~TIM5 | 向上/向下 | 4 | 无 |
基本 | TIM6,TIM7 | 只有向上 | 0 | 无 |
共性
属性 | 值 |
---|---|
计数器分辨率 | 16位 |
预分频系数 | 1~65535 |
产生DMA | 可以 |
Servo需要输出频率为 50HZ 的波
//以下CCR是在舵机可用占空比条件下的区间
//CCR可调范围(推荐):500 ~ 2500
TIM_TimeBaseStructure.TIM_Prescaler = (72-1);//72M / 72 =1M
TIM_TimeBaseStructure.TIM_Period = (20000 - 1);//1M / 20K =50
//CCR可调范围:5 ~ 25
TIM_TimeBaseStructure.TIM_Prescaler = (7200-1);//72M / 7.2K =10k
TIM_TimeBaseStructure.TIM_Period = (200 - 1);//10k / 0.2k =50
舵机角度 | 置高(等比缩小的CCR值)时间(一个周期为20ms) | 占空比 |
---|---|---|
0 | 0.5ms | 2.5% |
90 | 1.5ms | 7.5% |
180 | 2.5ms | 12.5% |
注:实际用在小车上的舵机只会在大约70到120度改变
可以算一下舵机转1度需要改变多少CCR