stm32学习合集(标准库)
注意点:PA15,PB3,PB4是调试端口,一般不用做GPIO口做输入输出,如果要用的话,那就是要经过特殊的初始化,以下是特殊的处理步骤,但是我还没有试过;基本定时器预分频器:如果内部时钟是72M,那么这个分频器就可以对72MHz进行分频,计数器:就是计数自动重装寄存器:就是当计数器计到自动重装寄存器里面存储的值了,就会产生中断,同时计数器还会清零;
目录
PWMI模式测量占空比和频率(PMWI也就是PWM输入模式)
DMA转运数组(简单的从一个数组的数据搬运到另一个数组中去)
库函数单词缩写的小知识
“Cmd”在STM32的标准外设库(SPL)中通常是“Command”的缩写,用来表示控制某个外设或功能的开启或关闭。 比如TIM_Cmd(TIM2, ENABLE)启动定时器。
ITConfig中的IT应该指的是中断(Interrupt),所以ITConfig应该是Interrupt Configuration的缩写。
ETR 是 External Trigger(外部触发) 的缩写,特指定时器的外部触发输入功能。它允许定时器通过外部信号(如脉冲、方波等)来触发特定操作
在 STM32 的定时器(Timer)中,Prescaler(预分频器) 是一个关键参数,用于对定时器的输入时钟源进行分频,从而降低计数器的计数频率。它的本质是调整定时器的“时间基准”,直接影响定时器的计时精度和最大定时周期。PSC是他的简写
ARRP 通常指 Auto-Reload Register Preload(自动重装载寄存器预装载),它是定时器的重要功能之一,用于控制自动重装载寄存器(ARR)的更新机制。
GPIO_Mode_Out_PP 表示 推挽输出模式(Push-Pull Output),是GPIO配置中最常用的输出模式之一。
程序烧录后要按下复位按键程序才运行的问题

外设硬件部分
正交编码器(旋钮)
正转时A相的相位提前于B相的相位90度;
反转时A相的相位滞后于B相的相位90度;

舵机(SG90)

直流电机


用外部中断计数:在设置他的初始化函数时跟正常的外部中断初始化没有什么不同,江科大它用了中断0和中断1,所以他们的中断优先级可以更细致,可以单独给每一个中断优先级赋值,但是我用的是13和14他们的中断优先级配置分开不了,中断服务函数也是一个中断服务函数,但是中断服务函数没有关系,可以在里面分开,但是中断优先级就不能精确到每一个中断通道了,但是好像也没有关系,正交编码器还是可以正常显示;
#include "stm32f10x.h" // Device header
int16_t num;
void Encoder_Init(void)
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//打开GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//开启AFIO外设的时钟
//初始化GPIO
GPIO_InitTypeDef GPIO_Initstruture;
GPIO_Initstruture.GPIO_Mode=GPIO_Mode_IPU;//上拉输入
GPIO_Initstruture.GPIO_Pin=GPIO_Pin_13|GPIO_Pin_14;
GPIO_Initstruture.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_Initstruture);
//初始化AFIO 选择外部中断源
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource13);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
//初始化EXTI 具体中断是怎么触发的
EXTI_InitTypeDef EXTI_Initstructure;
EXTI_Initstructure.EXTI_Line=EXTI_Line13|EXTI_Line14;
EXTI_Initstructure.EXTI_LineCmd=ENABLE;
EXTI_Initstructure.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_Initstructure.EXTI_Trigger=EXTI_Trigger_Rising;
EXTI_Init(&EXTI_Initstructure);
//初始化NVIC 配置中断优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_Initstructure;
NVIC_Initstructure.NVIC_IRQChannel=EXTI15_10_IRQn;
NVIC_Initstructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Initstructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_Initstructure.NVIC_IRQChannelSubPriority=0;
NVIC_Init(&NVIC_Initstructure);
}
int16_t Encoder_Get_num(void)
{
uint16_t med_date;
med_date=num;
num=0;
return med_date;
}
void EXTI15_10_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line13)==SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==SET)
{
if((GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_14)==RESET))
{
num++;
}
}
EXTI_ClearITPendingBit(EXTI_Line13);
}
if(EXTI_GetITStatus(EXTI_Line14)==SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_14)==SET)
{
if((GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==RESET))
{
num--;
}
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
}
外设时钟
每个外设都有时钟,不用的外设可以不用开启时钟,节省性能;
下面是各个总线的外设,
/*
AHB外设总线:
DMA1,DMA2,SRAM,FLITF,CRC,FSMC,SDIO
*/
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC,ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC,DISABLE);
/*
APB1外设总线:
TIM2,TIM3,TIM4,TIM5,TIM6,TIM7,TIM12,TIM13,TIM14,WWDG
SPI2,SPI3,USART2,USART3,UART4,UART5,I2C1,I2C2,USB,CAN1,CAN2,BKP,PWR,DAC,CEC,
*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2,DISABLE);
/*
APB2外设总线:
AFIO,GPIOA,GPIOB,GPIOC,GPIOD,GPIOE,GPIOF,GPIOG,ADC1,ADC2
TIM1,SPI1,TIM8,USART1,ADC3,TIM15,TIM16,TIM17,TIM9,TIM10,TIM11
*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, DISABLE);
GPIO
GPIO的引脚定义表

注意点:PA15,PB3,PB4是调试端口,一般不用做GPIO口做输入输出,如果要用的话,那就是要经过特殊的初始化,以下是特殊的处理步骤,但是我还没有试过;

//----------------------------------------------------------------------------------------------------------------------------//
GPIO的八种模式


上拉输入:IO口没有操作的时候就默认高电平1
下拉输入:IO口没有操作的时候就默认低电平0
浮空输入: 0:IO口没有操作的时候不确定是1还是0,
1. 外部电路已经具备上拉或下拉电阻,不需要内部电阻。
2. 用于某些通信协议,如I2C,需要外部上拉的情况。
3 . 当信号源本身能够明确驱动高低电平,比如数字传感器输出明确的高或低,无需额外电阻。
4. 高阻抗状态的应用,比如多路复用信号,避免内部电阻干扰。
模拟输入:输入信号直接跳过密特触发器,引脚直接接入内部ADC,输入的是模拟量,所以这个端口就是ADC模式的专属输入模式
推挽输出:单片机通过两个Mos管,具有输出高电平1和低电平0的能力;
开漏输出:只有一个MOS管开启,所以只能输出低电平0,不能够输出高电平1,输出高电平1的时候是高阻状态,这个模式常用于通信协议的驱动方式,比如iic通信,1:可以避免多机通信时的相互干扰,2:可以输出5V,但是要外接上拉电阻的5V电源,当开漏模式下,单片机输入高电平1的时候,就能使外部的上拉电阻上拉至5V;
复用推挽输出:跟普通的推挽输出一样,但是这个时候它输出是由片上外设控制的,
复用开漏输出:跟普通的开漏输出一样,但是这个时候它输出是由片上外设控制的,

GPIO的初始化的时候,它有个参数speed速度,只在输出模式下有用,这个参数是调制GPIO高低电平的翻转速度的,要求不高的话,直接50MHz就可以了,
//----------------------------------------------------------------------------------------------------------------------------//
GPIO输入
按键输入:有无外部上拉电阻或下拉电阻的情况

//----------------------------------------------------------------------------------------------------------------------------//
外部中断学习-对射式红外传感器计次
外部中断初始化
1:首先按照图的流程设置初始化寄存器

第一步设,配置RCC时钟,将之使能
RCC开启时钟,就是把外设的时钟将之打开,这里有GPIO,AFIO,EXTI,三个外设,但是EXTI不用开启时钟,可能是因为像让EXTI常开使用,NVIC是内设不用开启时钟,所以第一步就只需要将GPIO和AFIO的时钟打开,代码如下
第二步,配置GPIO,选择我们的GPIO模式为输入模式(配为上拉输入)

第三步;配置AFIO,(指定中断源)实质上是数据选择器
AFIO的函数是在GPIO.h文件里面的,代码如下

第四步;配置EXTI,选择我们的触发方式,上升沿/下降沿/双边沿
1:所有的GPIO口都能触发中断,但相同的Pin口不能同时触发中断,比如PA1,PB1,PC1只能有一个触发中断,
2:触发响应方法分为:中断响应和事件响应
中断响应:中断直接送到中断函数进行处理对应的操作
事件响应:中断送到片上外设进行处理,属于中断与片上外设的联动
第五步:配置NVIC,给我们的中断选择一个合适的优先级,
NVI的函数是在杂项库文件里面
1:首先优先级有抢占优先级和响应优先级,抢占优先级优先于响应优先级,
2: 抢占优先级可以中断嵌套也就是可以优先执行,
响应优先级高的可以优先排队
3:抢占优先级:高抢占优先级可以打断正在执行的低抢占优先级的中断,立刻执行高优先级的中断;
4:响应优先级:当抢占优先级相同时,响应优先级高的先执行;(这里的先执行是中断排队排在前面,并不会打断现在的中断,然后立刻执行,是优先插队的意思)
5:抢占优先级和响应优先级都相同的时候,自然优先级高的先执行,
6:自然优先级:中断向量表中的优先级;
7:数值越小,代表优先级越高;
NVIC配置优先级这里有一点疑惑,
然后就是中断服务函数了,下面就是一个基本框架

定时器
C8T6定时器简介

基本定时器

预分频器:如果内部时钟是72M,那么这个分频器就可以对72MHz进行分频,
计数器:就是计数
自动重装寄存器:就是当计数器计到自动重装寄存器里面存储的值了,就会产生中断,同时计数器还会清零;
主模式触发DAC:
定时器触发事件中断,这个中断直接连接到TRGO(Trigger output)也就是DAC输出,就可以实现全硬件触发DAC输出,大大节省CPU的负担;

通用定时器

1:基本定时器只有向上计数的功能,但是通用和高级定时器还支持向下和中央对齐功能;
2:相比于基本定时器,方波信号作为时钟)还具有外部时钟(作为基准时钟的,这个时候就也可以对外部的方波信号计数,
高级定时器

高级定时器主要增加的是对运行三相无刷电机的功能,感觉是通用定时器相差不大,
影子寄存器
ARPE寄存器是影子寄存器的使能位;影子寄存器的作用就是决定更新事件是否跟随自动加载寄存器的值变化而马上变化;


RCC时钟树
一般stm32都会有四个时钟,一个是单片机的内部时钟(RC振荡器),一个是一个是外部时钟(石英晶体振荡器),还有一个是专门为RTC的时钟,一般为33M多,还有一个时钟是专门为看门狗准备的时钟;一般使用的是外部的石英晶体振荡器时钟,因为它更精确,但是如果外部的时钟坏掉了,这个时候内部的时钟就会接手,但是会比外部时钟慢大约十倍,因为外部时钟经过倍频是72M,内部时钟只有8M,一个跳变大约是一微妙;
定时器的主从触发模式

定时器中断应用(内部时钟)

代码
初始化
1:RCC开启时钟;
![]()
2:配置时基单元的时钟源-->开启内部时钟
但是不写的话也是没有问题的,上电就是默认内部时钟的;

3:配置时基单元,(用结构体)

其中TIM_ClockDivision这个参数是滤波的采样频率的,一分频就是直接用内部时钟频率滤波,去除信号的抖动,
period就是要写入的自动重装器ARR的值;
prescaler就是PSC预分频器的值;
PSC给少点,ARR给多点,就是以一个比较高的频率计比较多的数,反过来就是以一个比较低的频率计比较少的数
repetitioncounter是重复计数器的值;(这个是高级定时器才有的值)
如果要设置计数器的值的话,有Set_Counter的单独函数;

PSC和ARR的取值都要在0~65535之间;
下面是如何计算的;


4:配置输出中断控制--使能中断

5;配置NVIC--分配优先级

6:启动定时器

7:书写中断服务函数

定时器应用(外部时钟)
有一个需要特别注意的点TIM2定时器的外部时钟入口只能是PA0,
第一步:开启RCC时钟
第二步:初始化GPIO
第三步:配置外部时钟
第四步:配置时基单元
第五步:使能中断
第六步:配置NVIC
第七步:使能定时器
第八步:书写中断服务函数;
PWM输出比较
PWM简介:

PWM输出比较



PWM初始化的话,就是少了定时器配置中断的部分,多了一个输出比较寄存器的配置,
注意点:
1:输出PWM波的引脚要设置为复用推挽输出
第一步:初始化时钟:
第二步:配置GPIO口:
第三步:选择时钟源(一般是内部时钟)
第四步:配置时基单元
第五步:配置输出比较寄存器:
第六步:启动定时器:
第七步:书写一个单独给CCR赋值的函数,用来修改占空比:

最后的有个问题就是上电就会进去一次中断,导致计数直接就加1,原因没有听明白,但是解决方法会;
就是直接加个清楚中断标志位的,配置好时基单元不能让他直接就进去中断,这个好像跟那个影子寄存器有关系;

PWM简单驱动舵机
主要的注意点应该是要计算占空比的时候,
首先周期为20ms,所以周期的倒数就是频率,但是这个1除以20,这个1的单位是秒的,要换单位,就是1000/20=50,频率就是50;然后根据公式就是计算出来ARR和PSC;

然后就是单独给舵机整一个函数,这样子就比较的独立,模块化,

PWM驱动电机
驱动电机和驱动舵机几乎没有什么两样,但是多了两个控制方向的引脚;这两个引脚只要直接接到单片机IO口上就行了,正反转靠的就是这两个引脚,其他的好像没有什么细节了;

输入比较
输入捕获获取频率(测周法)
//

测周法:原理就是测量两个上升沿中间的时间,也就是一个周期,周期的倒数就是频率,而周期是计次N乘上标准频率Fc就能得到周期T,反过来就是频率; 适合低频信号,但是这个方法用了一个周期就出结果了,并不是平均值,所以她受噪声的干扰比较的大,
测频法:原理就是这个闸门时间一般设置为1秒,然后开始测量这个时间内出现的上升沿或者下降沿,在一秒的闸门时间内,出现了多少个上升沿或者下降沿就是频率,当然这个闸门时间不固定,一秒时间是最好算的,设置为其他时间就只需要按照公式进行变换就可以了,频率的定义就是在一秒的时间内出现了多少的上升沿或下降沿,适合高频信号,测频法测量的是很多个周期内的平均值,自带均值滤波,所以能很好的抵御噪声;


主模式:这些信号源可以去触发其他的操作
从模式: 设置了从模式就可以接收到其他的信号,来触发相应的操作,在这里测频法这里主要是要把计数器的计数值清零;
第一步:
开启时钟,GPIO的时钟,TIM的时钟

第二步:
GPIO初始化,模式一般是上拉输入

第三步:
配置时基单元

第四步:
配置输入捕获单元()

TIM_ICFilter:输入滤波器参数,可以过滤信号抖动 ,滤除噪声,并不会改变信号的频率,
Prescaler(PSC):直接对信号进行分频,会对信号的频率进行改变;
第五步:
选择从模式的触发源

第六步:
选择触发之后的操作;()

第七步:
开启定时器

最后再编写一个计算频率的函数

//
还有一个就是在PWM函数里面还要加一个单独改变PSC的函数;通过PSC改变频率;
公式:
频率Freq = CK_PSC / (PSC + 1) / (ARR + 1)
占空比Duty = CCR / (ARR + 1)


PWMI模式测量占空比和频率(PMWI也就是PWM输入模式)
首先原理是,结合上面的代码,已经把一个周期的时间都存在了CCR1里面,这个时候我们再调用一个函数,TIM_PWMIConfig(TIM3,&TIM_ICInitstructure);//把通道2配置为下降沿,交叉,感觉就是直接对通道1取反;这个样子就把一个上升沿到下降沿的时间存在CCR2里面,这个时候把CCR2除以CCR1就能得到占空比,但是这个时候还是一个0~1的数,我们在放大100倍,就能直接观察到百分数,
下面是对于上面的测量频率的函数有修改的部分,主要就是加了一个函数,
//配置输入捕获单元
TIM_ICInitTypeDef TIM_ICInitstructure;
TIM_ICInitstructure.TIM_Channel=TIM_Channel_1;
TIM_ICInitstructure.TIM_ICFilter=0xf;//滤波器
TIM_ICInitstructure.TIM_ICPolarity=TIM_ICPolarity_Rising;//它决定了捕获是在信号的上升沿、下降沿还是双边沿触发。
TIM_ICInitstructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;
TIM_ICInitstructure.TIM_ICSelection=TIM_ICSelection_DirectTI;
TIM_ICInit(TIM3,&TIM_ICInitstructure);
TIM_PWMIConfig(TIM3,&TIM_ICInitstructure);//把通道2配置为下降沿,交叉,感觉就是直接对通道1取反;
但是要注意一点:这个函数只支持通道1和通道2

//--------------------------------------------------------------------------------------------------------------------//
根据这两条公式;首先在PWM模块函数里面,把(ARR+1)设置为100;也就是把ITM_peripd设置为100;这个样子就为后面的占空比计算方便;这个样子CCR设置为几占空比就是为几;下面是在PWM函数中的调节占空比的函数;

然后就是因为确定了(ARR+1)等于100;这个时候再以我们要设置的频率是多少,就能够算出来PSC解为多少,设置为多少,一般是72的整十倍,下面是调节频率的函数

//--------------------------------------------------------------------------------------------------------------------//
然后就到了IC函数里面的时基单元里面的参数配置;
ARR,也就是自动重装载值,因为要尽可能地极多的数,而不至于溢出,所以,ARR设置为最大65536-1;
再到PSC,PSC决定的就是基准频率Fc,我们把它设置为1MHz,PSC参数配置为72-1;这里为什么设置为1MHz,有点模糊,按理说可以设置为任何数
然后就是就一个计算占空比的函数

CCR寄存器;在输出比较和输入捕获里面的作用都不一样,但是他们是同一个寄存器好像;
//-----------------------------------------------------------------------------------------------------------------------------//
定时器的编码器专用接口
定义及用法
所以只能用两个特定的接口,一个定时器只有两个编码器接口,一个编码器接口只能用特定的通道;

基本流程图

去除噪声和毛刺的方法

极性选择:这种说明一下,就是反相和不反相的问题,初始化的时候是有一个极性选择的,在其他模块的时候这个极性选择是选择上升沿有效还是下降沿有效,但是在编码器这里是如果是上升沿的话,计时不反相,高电平进来就是高电平,低电平进来就是低电平,如果极性选择的是下降沿的话,高电平进来就是低电平,可以用来方便的改变自增还是自减的方向;

配置初始化
第一步:RCC开启时钟
第二步:配置GPIO,配置为输入模式
第三步:配置时基单元
第四步:配置输入捕获单元
//配置时基单元
TIM_ICInitTypeDef TIM_ICInitstructure;
TIM_ICStructInit(&TIM_ICInitstructure);
TIM_ICInitstructure.TIM_Channel=TIM_Channel_1;
TIM_ICInitstructure.TIM_ICFilter=0xf;
TIM_ICInit(TIM3,&TIM_ICInitstructure);
TIM_ICInitstructure.TIM_Channel=TIM_Channel_2;
TIM_ICInitstructure.TIM_ICFilter=0xf;
TIM_ICInit(TIM3,&TIM_ICInitstructure);
第五步:配置编码器接口模式
//配置编码器接口模式
TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
第六步:开启时钟:
新内容就是加了一条配置编码器接口模式的函数,因为配置编码器模式的函数又对配置时基单元中的极性重复配置,所以这个需要在配置编码器模式那里配置一下就可以了;
//-----------------------------------------------------------------------------------------------------------------------------//
按键
按键初始化
要用上拉输入模式

按键会一直返回键码的问题

按键进阶(单击/双击/长按)
//-----------------------------------------------------------------------------------------------------------------------------//
ADC
标志位:
校准复位标志位:RSTCAL

校准完成标志位:CAL

注入组转换完成标志位: JEOC

规则组转换完成标志位:EOC

ADC简介
采样深度:
12位逐次逼近型ADC,这里的12位就是采样深度;采样深度就决定了误差;

逐次逼近型

逐次逼近型工作原理
首先,内部基准电压与外部测量电压进行比较外部电压较大的就往结果寄存器里面写1,比较的小就写0,如此往复,就能够往结果寄存器里面写入一个值,在这里一般是使用的是二分法,比较个12次就能够得出一个大概值,

采样保持电路
分为采样和保持:
首先就是采样:看下图,我们采样模拟信号的一个点,闭合开关,外部电压就会对电容充电,这个时候就完成了采样的程序,
接着就是保持过程:采样完成之后,断开开关,这个时候电容已经充满了电,因为开关断开,比较器也因为虚短虚断,电容里面的电流一个地方都流不出去,这个时候就完成了保持,

采样时间和转换时间
输入到ADC的时钟频率不能超过14MHz,所以在配置ADC时钟的时候至少要选择6分频,ADC的时钟频率就为12MHz,

采样时间:
开关闭合的时间长度,信号源内阻越大,电流就越小,ADC的电容充电就充得慢,


在手册里面可以找到公式,

转换时间:
对采样点进行转换所消耗的时间,


逐次逼近型ADC:

转换时间:
采样保持:因为后面的量化编码需要时间,如果在量化编码的时候,电压还在不断地变化的话,不利于量化编码,所以需要采样保持,保持一段时间的电压稳定,
量化编码:把电压进行逐次逼近的过程,需要一段时间;用内部的量化电压不断比较测量电压,不相同就一直调整量化电压,直至相同,所以需要时间

例子

结构框图:
注意这里的外部触发条件,好像是只有这几个条件能触发,
规则组/常规组:用于常规转换,

注入组: 用于突发事件的转换,优先级更高,

例子(常规组和注入组都设置)
结果就是:常规组的触发采集的优先级没有这么高,当注入组来的之后,必须执行注入组的采集先;

ACD初始化(单通道)(规则组)

ADC通道只能使用规定的10个通道,具体是哪个引脚可以查表
ADC通道:10个外部通道和2个内部通道;外部通道应该是PA0~PA7和PB0,PB1十个内部通道,内部通道是温度和VREFINT(内部电压),
触发控制:


第一步:开启RCC时钟(时钟包括GPIO,ADC,还有一个ADCCLK)
ADCCLK时钟一般设置为12MHz,6分频;
第二步:配置GPIO,端口要配置为模拟输入模式
第三步:配置多路开关 就是配置通道

最后一个参数的选择原因及意义


第四步:配置ADC转换器:

模式选择:
连续模式
非连续模式:
扫描模式:
非扫描模式:
所以相互配合就有4种模式:
单次转换,非扫描模式:触发一次就转换一次,然后就会停止,EOC标志位置1,如果想要再继续转换那只能再触发一次,

连续转换,非扫描模式:因为还是非扫描模式,所以序列表里面只会填一个通道;但是是连续模式,所以只需要触发一次,就可以一直转换,不用判断是否转换完成,直接读取AD值就可以,
单次转换,扫描模式:就是说触发一次就能连续扫描序列表里面的一群通道,扫描模式就能够往序列里面添加多个通道,但是因为是单次转换,所以触发一次就只能扫面一次,之后就停止了,EOC置1,要是想继续转换,那就要再触发一次,但是扫描模式还有一个缺点,就是要及时的把数据移走,不然前面通道的AD值就会被覆盖,通常都是要结合DMA来使用,

连续转换,扫描模式:这个模式只需要触发一次,就能够一直扫描列表里面的一群通道;

数据对齐: 一般使用右对齐

第五步:开启ADC
第六步:校准ADC



最后就是读取数据,通过判断EOC标志位来判断是否转换完成;1完成 0未完成

ADC转换多通道(规则组)(定时器主模式TRGO触发转换)
代码如下
定时器:定时器这里需要注意的是:与正常的定时器中断触发不一样的是,用主模式TRGO硬件全自动触发,就不用配置关于中断的函数,根本就没有用到中断,
#include "stm32f10x.h" // Device header
void Timer3_Iinit(void)
{
//开启RCC时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
//指定时钟源
TIM_InternalClockConfig(TIM3);
//配置时基单元 //配置为1ms
TIM_TimeBaseInitTypeDef TIM_Initstructure;
TIM_Initstructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_Initstructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_Initstructure.TIM_Period=100-1;
TIM_Initstructure.TIM_Prescaler=720-1;
TIM_Initstructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM3,&TIM_Initstructure);
//配置主模式触发源
TIM_SelectOutputTrigger(TIM3,TIM_TRGOSource_Update);
//使能定时器
TIM_Cmd(TIM3,ENABLE);
}
ADC函数:
最重要的是加了一句使能外部中断触发的函数,一开始一直转换不了,原来是因为这句函数没有使能,
#include "stm32f10x.h" // Device header
void z_ADC_Init(void)//ADC初始化
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//开启ADCCLK
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//配置GPIO
GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Mode=GPIO_Mode_AIN;
GPIO_Initstructure.GPIO_Pin=GPIO_Pin_0;//A0输入模拟值
GPIO_Initstructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_Initstructure);
//配置选路开关 选通道
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
//配置ADC转换器
ADC_InitTypeDef ADC_Inirstructure;
ADC_Inirstructure.ADC_ContinuousConvMode=DISABLE;
ADC_Inirstructure.ADC_DataAlign=ADC_DataAlign_Right;
ADC_Inirstructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_T3_TRGO;//Timer3的TRGO输出触发
ADC_Inirstructure.ADC_Mode=ADC_Mode_Independent;
ADC_Inirstructure.ADC_NbrOfChannel=1;
ADC_Inirstructure.ADC_ScanConvMode=DISABLE;
ADC_Init(ADC1,&ADC_Inirstructure);
//最重要的一句,使能外部TRGO触发转换
ADC_ExternalTrigConvCmd(ADC1,ENABLE);
//使能ADC
ADC_Cmd(ADC1,ENABLE);
//校准ADC
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1)==SET);
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1)==SET);
}
uint16_t ADC1_Timer3_TRGO_GetDat(void)
{
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);
return ADC_GetConversionValue(ADC1);
}
效果
ADC多通道(规则组)(不用DMA)
ADC函数
#include "stm32f10x.h" // Device header
void Z_ADC_Init(void)
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
//配置ADCCLK
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//配置GPIO
GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Mode=GPIO_Mode_AIN;
GPIO_Initstructure.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
GPIO_Initstructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_Initstructure);
//配置ADC转换器
ADC_InitTypeDef ADC_Initstructure;
ADC_Initstructure.ADC_ContinuousConvMode=DISABLE;
ADC_Initstructure.ADC_DataAlign=ADC_DataAlign_Right;
ADC_Initstructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
ADC_Initstructure.ADC_Mode=ADC_Mode_Independent;
ADC_Initstructure.ADC_NbrOfChannel=1;
ADC_Initstructure.ADC_ScanConvMode=DISABLE;
ADC_Init(ADC1,&ADC_Initstructure);
//使能ADC
ADC_Cmd(ADC1,ENABLE);
//校准ADC
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1)==SET);
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1)==SET);
}
uint16_t ADC1_GetDat(uint8_t ADC_Channel)
{
//配置多路开关
ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5);
//软件触发
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
//等待转换完成
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);
return ADC_GetConversionValue(ADC1);
}
主函数
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "ADC.h"
#include "Delay.h"
uint16_t ADC_0;
uint16_t ADC_1;
uint16_t ADC_2;
uint16_t ADC_3;
int main(void)
{
OLED_Init();
Z_ADC_Init();
OLED_ShowString(0,0,"ADC_0:",OLED_8X16);
OLED_ShowString(0,16,"ADC_1:",OLED_8X16);
OLED_ShowString(0,32,"ADC_2:",OLED_8X16);
OLED_ShowString(0,48,"ADC_3:",OLED_8X16);
while (1)
{
ADC_0=ADC1_GetDat(ADC_Channel_0);
ADC_1=ADC1_GetDat(ADC_Channel_1);
ADC_2=ADC1_GetDat(ADC_Channel_2);
ADC_3=ADC1_GetDat(ADC_Channel_3);
OLED_ShowNum(48,0,ADC_0,5,OLED_8X16);
OLED_ShowNum(48,16,ADC_1,5,OLED_8X16);
OLED_ShowNum(48,32,ADC_2,5,OLED_8X16);
OLED_ShowNum(48,48,ADC_3,5,OLED_8X16);
OLED_Update();
Delay_ms(100);
}
}
定时器TRGO触发ADC采集连续模式(注入组)
ADC_Init();函数是配置基本的ADC转换参数的,如果是规则组的话,就设置ADC_Init()就够了,但是如果是注入组的话,还需要另外设置注入组的参数:下面是基本的函数:

代码如下:
以下该代码实现了用定时器TRGO主模式触发ADC转换两路ADC,其中A0测量的是电位器的电压AD值,A1测量了光敏传感器的AD值,
需要特别注意的是,注入组的通道数量和通道与序列表的匹配要单独设置,有函数能配置(上表),还有就是配置了触发条件是TRGO后,要使能触发条件,还有一件事,就是注入组转换完成后,JEOC标志位会置1,这个时候要软件清除标志位,
#include "stm32f10x.h" // Device header
#include "OLED.h"
//-------------------------------------------------//
uint16_t inject1_dat;//通道1
uint16_t inject2_dat;//通道2
//-------------------------------------------------//
void Timer_Init(void);//定时器初始化
void ADC_Inject_Init(void);//ADC注入组初始化
uint16_t ADC_GetDat(uint8_t ADC_InjectedChannel);//ADC_InjectedChannel:通道号
//-------------------------------------------------//
int main(void)//主函数
{
OLED_Init();
Timer_Init();
ADC_Inject_Init();
OLED_ShowString(0,0,"hello world!",OLED_8X16);
while (1)
{
//采样
inject1_dat=ADC_GetDat(ADC_InjectedChannel_1);
inject2_dat=ADC_GetDat(ADC_InjectedChannel_2);
//显示
OLED_ShowNum(0,16,inject1_dat,5,OLED_8X16);
OLED_ShowNum(0,32,inject2_dat,5,OLED_8X16);
OLED_Update();
}
}
//-------------------------------------------------//
void Timer_Init(void)//定时器初始化 TIM4的TRGO
{
//开启RCC时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);
//指定时钟源
TIM_InternalClockConfig(TIM4);
//配置时基单元 设置为1MS
TIM_TimeBaseInitTypeDef TIM_Initstructure;
TIM_Initstructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_Initstructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_Initstructure.TIM_Period=100-1;
TIM_Initstructure.TIM_Prescaler=720-1;
TIM_Initstructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM4,&TIM_Initstructure);
//配置外部触发主模式
TIM_SelectOutputTrigger(TIM4,TIM_TRGOSource_Update);
//使能定时器
TIM_Cmd(TIM4,ENABLE);
}
//-------------------------------------------------//
void ADC_Inject_Init(void)//ADC注入组初始化
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
//配置ADCCLK
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//配置GPIO输入
GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Mode=GPIO_Mode_AIN;
GPIO_Initstructure.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1;
GPIO_Initstructure.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_Init(GPIOA,&GPIO_Initstructure);
//配置ADC转换基本参数
ADC_InitTypeDef ADC_Initstructure;
ADC_Initstructure.ADC_ContinuousConvMode=DISABLE;
ADC_Initstructure.ADC_DataAlign=ADC_DataAlign_Right;
ADC_Initstructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//这里是配置规则组的,所以找不到TIM4_TRGO
ADC_Initstructure.ADC_Mode=ADC_Mode_Independent;
ADC_Initstructure.ADC_NbrOfChannel=2;//这个是规则组的通道 配置了也没有用
ADC_Initstructure.ADC_ScanConvMode=ENABLE;
ADC_Init(ADC1,&ADC_Initstructure);
//配置ADC注入组参数 通道长度
ADC_InjectedSequencerLengthConfig(ADC1,2);
//配置多路开关 填通道到序列表
ADC_InjectedChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_13Cycles5); //配置多路开关 选择通道 填通道
ADC_InjectedChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_13Cycles5); //配置多路开关 选择通道 填通道
//配置ADC触发方式
ADC_ExternalTrigInjectedConvConfig(ADC1,ADC_ExternalTrigInjecConv_T4_TRGO);
//使能ADC触发方式
ADC_ExternalTrigInjectedConvCmd(ADC1,ENABLE);
//使能ADC
ADC_Cmd(ADC1,ENABLE);
//校准ADC 4步骤:复位 检查复位完成 开始校准 检查校准完成
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1)==SET);
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1)==SET);
}
//-------------------------------------------------//
uint16_t ADC_GetDat(uint8_t ADC_InjectedChannel)//ADC_InjectedChannel:通道号
{
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_JEOC)==RESET);//等待转换完成
ADC_ClearFlag(ADC1,ADC_FLAG_JEOC);//必须清楚软件清楚标志位
return ADC_GetInjectedConversionValue(ADC1,ADC_InjectedChannel);
}
DMA
DMA简介

存储器映像

ROW:掉电不丢失
RAW:掉电丢失
结构框图:

仲裁器:
DMA1有多个通道,但是只有一条数据总线,产生冲突了,就用仲裁器进行优先级设置:


数据宽度:
字节:8位,uint8_t大小的数据
半字:16位;uint16_t大小的数据
字:32位,uint32_t大小的数据
地址是否自增:是的话就是DMA转运一次,地址就加加,但是ADC的话,规则组就只有一个寄存器DR,所以不用自增,
注意:软件触发(连续触发,并不是ADC的那个软件触发,它开了之后就会一直触发),所以软件触发,自动重装器的循环模式不能同时使用,不然DMA就会停不下来;软件触发一般用在存储器到存储器的转运,因为存储器到存储器的转运不需要时机,
注意:写传输计数器是,必须关闭DMA,就是DMA_Cmd函数要写DISABLE,关闭了之后才能写传输计数器,然后再开启DMA;
DMA请求信号:
en位是总开关,也就是DMA_Cmd();函数
M2M位是决定是软件触发还是硬件触发的,写1是软件触发,0是硬件触发,这里的硬件就是ADC,定时器,串口等;
注意:还有一点不一样就是:一个硬件的触发源必须选择它特定的通道;但是软件触发就可以随便选择通道;还有就是某个硬件要使用DMA,就打开它对应的DMA使能开关;比如ADC,就会有函数是ADC_DMACmd();函数,要注意使能

标志位:

GL:全局标志位
TC:转运完成标志位
HT:转运过半完成标志位
TE:转运错误标志位

上
表说:0是空闲状态,1产生了标志位:
循环模式和存储器到存储器模式(自然模式)
注意:循环模式和软件触发不能同时使用
用下面的这个函数设置:


DMA转运数组(简单的从一个数组的数据搬运到另一个数组中去)
第一步:开启RCC时钟

第二步:配置DMA初始化

第三步:开启DMA

但是我们一般都指定什么时候就开始DMA转换,比如定时器中断,外部中断等,所以我们会单独设置一个函数用来开启DMA数据转运;DMA也是和其他的外设绑定使用的,比如ADC;
下面是一个简单的示例,和注意事项

ADC多通道结合DMA
我的感觉就是难点就是要选择ADC的连续模式和扫描模式的选择,deep seek解释,有了一点理解
2025-5-22-已理解ADC的连续模式和扫描模式;

ADC每个通道转换完成之后都会触发ADC_DMA硬件触发,所以就是每个通道转换完成DMA都会自动的转运出去,防止DR寄存器的数据被覆盖;



#include "stm32f10x.h" // Device header
uint16_t Adc_dat[4];
void yADC_Init(void)
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//配置ADCCLK
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//配置GPIO
GPIO_InitTypeDef GPIOA_Initstructure;
GPIOA_Initstructure.GPIO_Mode = GPIO_Mode_AIN;
GPIOA_Initstructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;//四通道
GPIOA_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIOA_Initstructure);
//配置ADC多路开关 ADC通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_13Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_13Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_13Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_13Cycles5);
//配置ADC_Init基本参数
ADC_InitTypeDef ADC_Initstructure;
ADC_Initstructure.ADC_ContinuousConvMode = ENABLE;
ADC_Initstructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_Initstructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_Initstructure.ADC_Mode = ADC_Mode_Independent;
ADC_Initstructure.ADC_NbrOfChannel = 4;
ADC_Initstructure.ADC_ScanConvMode = ENABLE;
ADC_Init(ADC1, &ADC_Initstructure);
//配置DMA
DMA_InitTypeDef DMA_Initstructure;
DMA_Initstructure.DMA_MemoryBaseAddr = (uint32_t)Adc_dat;
DMA_Initstructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_Initstructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_Initstructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_Initstructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_Initstructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_Initstructure.DMA_Priority = DMA_Priority_Medium;
DMA_Initstructure.DMA_BufferSize = 4;
DMA_Initstructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_Initstructure.DMA_M2M = DMA_M2M_Disable;
DMA_Initstructure.DMA_Mode = DMA_Mode_Circular;
DMA_Init(DMA1_Channel1, &DMA_Initstructure);
//使能ADC
ADC_Cmd(ADC1, ENABLE);
//使能ADC_DMA
ADC_DMACmd(ADC1, ENABLE);
//使能DMA
DMA_Cmd(DMA1_Channel1, ENABLE);
//校准ADC
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1) == SET);
//启动触发条件
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
易错点: 注意要使能ADC,和ADC_DMA,和DMA三个;
还有就是ADC的转换完成标志位的检测,但是那个能理解,只需要记忆一下就好了,现在是不熟练的结果;
还有这里没有用到的DMA搬运计数器装载完成的标志位的检测,但是那也是不熟悉的原因;
USART串口
通信接口:

电平标准:

波特率发生器:

CH340模块:
跳线帽短接选择模式:




简介及术语介绍
波特率:每秒钟最多传输多少位,常用的就是9600,115200;
硬件数据流控:有两个引脚,这个模式的主要功能就是防止接收方接收数据接受不过来,能接收的时候就只一个标志位,提示能接受:发送方同理:
通用推挽输出和通用开漏输出:单片机直接对其进行读写,
复用推挽输出/复用开漏输出:单品机不直接对其引脚进行读写,把控制权交给芯片的其他模块:在这里就是串口发送要配置为此模式;
Tx发送引脚:
发送过程:首先发送一个字节八位,他是一下子就放到发送数据寄存器的,到了SBUF之后,就开始一位一位的从低位开始发送就解释了,串口发送数据为什么是从低位开始一位一位发送的,移位寄存器都是右移寄存器
Rx接收引脚:
接收过程:因为最先发送过来的是字节的最低位,但是第一位低位是存储在接受数据寄存器的最高位;接着第二位低位来的时候就会占据接收数据寄存器的最高位,原先的第一低位就会往接收数据寄存器的次高位挪;移位寄存器都是右移寄存器
全双工:可同时接受和发送,
半双工:同一时刻就只能有一个发,另一个就只能收:
同步模式:在标准的串口基础下,增加一根时钟线,把两个设备的时钟同步起来
TXE标志位:transmit Data register Empty;如果发送数据寄存器是空的,标志位就为1;
TC标志位:Transmit comolete 发送完成,当TDR空且移位寄存器也空时,TC就为1;
串口发数据
初始化函数
#include "stm32f10x.h" // Device header
void serial_Init(void)
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//配置GPIO
GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Initstructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_Initstructure);
//配置串口参数
USART_InitTypeDef USART_Initstructure;
USART_Initstructure.USART_BaudRate = 9600;
USART_Initstructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_Initstructure.USART_Mode = USART_Mode_Tx;
USART_Initstructure.USART_Parity = USART_Parity_No;
USART_Initstructure.USART_StopBits = USART_StopBits_1;
USART_Initstructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_Initstructure);
//使能串口
USART_Cmd(USART1, ENABLE);
}
void USART1_SendByte(uint16_t Byte)//发送一位字节 8比特
{
USART_SendData(USART1, Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
}
发送数组

发送字符串


需要注意的是;定义的是字符类型的指针
发送无符号数字

包装printf函数

在串口函数里面添加头文件

然后重定义fputc函数,就能够像c语言一样,直接用printf函数打印

但是会有缺点,就是只能绑定一个串口,然后就可以用一个函数sprintf打印,这个函数就不限串口

上面是未封装的sprintf函数
下面是封装的sprintf函数
封装前添加头文件
![]()

打印汉字
UTF-8加上一个参数
然后就可以照常打印汉字


串口收发数据
IIC通信

硬件电路
配置成开漏输出和两线都加一个上拉电阻的原因没有明白;

IIC时序基本单元
起始条件:
终止条件:

开始:
void IIC_Start(void)//开始
{
zz_IIC_W_SDA(1);//数据线SDA先拉高,SCL再拉低,防止与停止条件重合
zz_IIC_W_SCL(1);
zz_IIC_W_SDA(0);
zz_IIC_W_SCL(0);//SCL再拉低,方便后面的时序拼接
}
停止:
void IIC_stop(void)//停止
{
zz_IIC_W_SDA(0); //DS: 先确保SDA低
zz_IIC_W_SCL(1);
zz_IIC_W_SDA(1);
// zz_IIC_W_SCL(0); //DS:修正:在SDA拉高后,不需要再拉低SCL。停止后总线空闲,SCL和SDA都应高
// DS:停止后,SCL和SDA都高,总线空闲
}
发送一个字节:

下面是我的方法:
void IIC_send_Byte(uint8_t Byte)//发送一个字节 高位先行
{
for(uint8_t i = 0; i<8; i++)
{
// if(Byte|(0x80>>i)) zz_IIC_W_SDA(1); //DS:应该是按位与操作,检查特定位是否为1。
if(Byte&(0x80>>i)) zz_IIC_W_SDA(1);
else zz_IIC_W_SDA(0);//DS:添加,我感觉也是要添加
zz_IIC_W_SCL(1);
zz_IIC_W_SCL(0);
}
}
下面的江科大的方法:
MyI2C_W_SDA(!!(Byte & (0x80 >> i)));这个函数即使不用把非零数转换为0或1,我感觉也可以,因为这个函数的参数只能是一位,所以非零就是1;
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++) //循环8次,主机依次发送数据的每一位
{
/*两个!可以对数据进行两次逻辑取反,作用是把非0值统一转换为1,即:!!(0) = 0,!!(非0) = 1*/
MyI2C_W_SDA(!!(Byte & (0x80 >> i)));//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
}
}
接收一个字节:

uint8_t IIC_Receive_Byte(void)//接收一个字节
{
uint8_t Byte = 0;
zz_IIC_W_SDA(1);
for(uint8_t i = 0; i<8; i++)
{
zz_IIC_W_SCL(1);//DS修改:先拉高SCL,然后在SCL高电平时读取SDA数据
if(zz_IIC_R_SDA() == SET) Byte|=(0x80>>i);
zz_IIC_W_SCL(0);
}
return Byte;
}

接收应答:
uint8_t IIC_Receive(void)//接收一位应答 主要是在查看从机是否在应答
{
uint8_t ACK;
zz_IIC_W_SDA(1);//DS:释放数据线
zz_IIC_W_SCL(1);
ACK = zz_IIC_R_SDA();//接收数据必须要在SCL高电平期间读取
zz_IIC_W_SCL(0);
return ACK;
}
发送应答:
void IIC_Send_Ack(uint8_t ACK)//发送一位应答 主要是在主机接收到从机发来的数据的时候
{
zz_IIC_W_SDA(ACK);
zz_IIC_W_SCL(1);
zz_IIC_W_SCL(0);
}
IIC时序:
指定地址写:
1:开始
2:发送设备地址+读还是写(一般都是写)
3:主机接收应答:(表示从机有应答)
4:发送地址(这个地址是设备内部的地址)
5:接收应答(表示从机有应答)
6:发送数据
7:主机接收应答,(表示从机有应答)
8:停止

当前地址读:
主机发送读指令后,就直接读当前地址的数据,这个当前地址,应该是如果以前没有进行写或读操作的话,那么读的就是直接就是0地址下的数据,但是当在当前地址读之前进行了写数据,那么这个时候的当前地址就会变为上个写数据地址的下一个地址;
1:开始
2:发送设备地址+读标志位
3:主机接收应答,(表示从机有应答)
4:主机直接读取数据
5:主机发送应答(表示收到数据)
6:停止

指定地址读;
1:开始
2:主机发送设备地址+写标志位
3:接收应答(表示从机有应答)
4:主机发送设备内部地址
5:主机接收应答(表示从机接收到了地址数据,从机有应答)
6:开始;
7:主机再次发送设备地址+读标志位
8:主机接收应答(表示从机有应答)
9:主机读取数据:
10:主机发送应答1(表示主机接收到了数据,但是不想从机再发数据了,如果是发应答0,那就是说从机继续给数据)
11:停止

底层代码编写
MPU6050
MPU6050基本简介



软件MPU6050初始化:
需要解除睡眠模式啥的
#define MPU6050_SMPLRT_DIV 0x19
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C
#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48
#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_PWR_MGMT_2 0x6C
#define MPU6050_WHO_AM_I 0x75
void MPU6050_Init(void)
{
zz_IIC_Init(); //先初始化底层的I2C
/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
MPU6050_Write_Given_address(MPU6050_PWR_MGMT_1, 0x01); //电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
MPU6050_Write_Given_address(MPU6050_PWR_MGMT_2, 0x00); //电源管理寄存器2,保持默认值0,所有轴均不待机
MPU6050_Write_Given_address(MPU6050_SMPLRT_DIV, 0x09); //采样率分频寄存器,配置采样率
MPU6050_Write_Given_address(MPU6050_CONFIG, 0x06); //配置寄存器,配置DLPF
MPU6050_Write_Given_address(MPU6050_GYRO_CONFIG, 0x18); //陀螺仪配置寄存器,选择满量程为±2000°/s
MPU6050_Write_Given_address(MPU6050_ACCEL_CONFIG, 0x18); //加速度计配置寄存器,选择满量程为±16g
}
完整代码
IIC部分:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#define IIC_SCL GPIO_Pin_10 //时钟线
#define IIC_SDA GPIO_Pin_9 //数据线
void zz_IIC_W_SCL(uint8_t Bitvalue)//操作时钟线 1拉高释放
{
GPIO_WriteBit(GPIOA, IIC_SCL, (BitAction)Bitvalue);
Delay_us(10);
}
void zz_IIC_W_SDA(uint8_t Bitvalue)//操作数据线 1拉高释放
{
GPIO_WriteBit(GPIOA, IIC_SDA, (BitAction)Bitvalue);
Delay_us(10);
}
uint8_t zz_IIC_R_SDA(void)//接收来自SDA的值
{
uint8_t Bit = 0;
Bit = GPIO_ReadInputDataBit(GPIOA, IIC_SDA);
Delay_us(10);
return Bit;
}
void zz_IIC_Init(void)
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO
GPIO_InitTypeDef GPIOA_Initstructure;
GPIOA_Initstructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIOA_Initstructure.GPIO_Pin = IIC_SCL|IIC_SDA;
GPIOA_Initstructure.GPIO_Speed =GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIOA_Initstructure);
GPIO_WriteBit(GPIOA, IIC_SCL, Bit_SET);//拉高时钟线
GPIO_WriteBit(GPIOA, IIC_SDA, Bit_SET);//拉高数据线
}
void IIC_Start(void)//开始
{
zz_IIC_W_SDA(1);//数据线SDA先拉高,SCL再拉低,防止与停止条件重合
zz_IIC_W_SCL(1);
zz_IIC_W_SDA(0);
zz_IIC_W_SCL(0);
}
void IIC_stop(void)//停止
{
zz_IIC_W_SDA(0); //DS: 先确保SDA低
zz_IIC_W_SCL(1);
zz_IIC_W_SDA(1);
// zz_IIC_W_SCL(0); //DS:修正:在SDA拉高后,不需要再拉低SCL。停止后总线空闲,SCL和SDA都应高
// DS:停止后,SCL和SDA都高,总线空闲
}
void IIC_send_Byte(uint8_t Byte)//发送一个字节 高位先行
{
for(uint8_t i = 0; i<8; i++)
{
// if(Byte|(0x80>>i)) zz_IIC_W_SDA(1); //DS:应该是按位与操作,检查特定位是否为1。
if(Byte&(0x80>>i)) zz_IIC_W_SDA(1);
else zz_IIC_W_SDA(0);//DS:添加,我感觉也是要添加
zz_IIC_W_SCL(1);
zz_IIC_W_SCL(0);
}
}
uint8_t IIC_Receive_Byte(void)//接收一个字节
{
uint8_t Byte = 0;
zz_IIC_W_SDA(1);//注意先释放总线
for(uint8_t i = 0; i<8; i++)
{
zz_IIC_W_SCL(1);//DS修改:先拉高SCL,然后在SCL高电平时读取SDA数据
if(zz_IIC_R_SDA() == SET) Byte|=(0x80>>i);
zz_IIC_W_SCL(0);
}
return Byte;
}
void IIC_Send_Ack(uint8_t ACK)//发送一位应答 主要是在主机接收到从机发来的数据的时候
{
zz_IIC_W_SDA(ACK);
zz_IIC_W_SCL(1);
zz_IIC_W_SCL(0);
}
uint8_t IIC_Receive_ACK(void)//接收一位应答 主要是在查看从机是否在应答
{
uint8_t ACK;
zz_IIC_W_SDA(1);//DS:释放数据线
zz_IIC_W_SCL(1);
ACK = zz_IIC_R_SDA();//接收数据必须要在SCL高电平期间读取
zz_IIC_W_SCL(0);
return ACK;
}
MPU6050部分:
#include "stm32f10x.h" // Device header
#include "zz_IIC.h"
#define MPU6050_address 0xd0 //代表设备地址加写
#define MPU6050_SMPLRT_DIV 0x19
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C
#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48
#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_PWR_MGMT_2 0x6C
#define MPU6050_WHO_AM_I 0x75
void MPU6050_Write_Given_address(uint16_t address, uint16_t Data)//指定地址写
{
IIC_Start();
IIC_send_Byte(MPU6050_address);
IIC_Receive_ACK();
IIC_send_Byte(address);
IIC_Receive_ACK();
IIC_send_Byte(Data);
IIC_Receive_ACK();
IIC_stop();
}
uint8_t MPU6050_Read_Given_address(uint16_t address)//指定地址读
{
uint8_t Data = 0;
IIC_Start();
IIC_send_Byte(MPU6050_address);
IIC_Receive_ACK();
IIC_send_Byte(address);
IIC_Receive_ACK();
IIC_Start();
IIC_send_Byte(MPU6050_address|0x01);
IIC_Receive_ACK();
Data = IIC_Receive_Byte();
IIC_Send_Ack(1);
IIC_stop();
return Data;
}
void MPU6050_Init(void)//初始化
{
zz_IIC_Init();
MPU6050_Write_Given_address(MPU6050_PWR_MGMT_1, 0x01);//解除睡眠模式1
MPU6050_Write_Given_address(MPU6050_PWR_MGMT_2, 0x00);//电源管理寄存器2,保持默认值0,所有轴均不待机
MPU6050_Write_Given_address(MPU6050_SMPLRT_DIV, 0x09);//采样率分频寄存器,配置采样率
MPU6050_Write_Given_address(MPU6050_CONFIG, 0x06);//配置寄存器
MPU6050_Write_Given_address(MPU6050_GYRO_CONFIG, 0x18);//配置寄存器陀螺 仪配置寄存器,选择满量程为±2000°/s
MPU6050_Write_Given_address(MPU6050_ACCEL_CONFIG, 0x18);//加速度计配置寄存器,选择满量程为±16g
}
uint16_t MPU6050_Get_ID(void)//获取地址号,ID号
{
return MPU6050_Read_Given_address(MPU6050_WHO_AM_I);
}
void MPU6050_Get_Dat(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ )
{
uint8_t Dat_H, Dat_L;
//X轴的加速度
Dat_H = MPU6050_Read_Given_address(MPU6050_ACCEL_XOUT_H);
Dat_L = MPU6050_Read_Given_address(MPU6050_ACCEL_XOUT_L);
*AccX = (Dat_H<<8) | Dat_L;
//Y轴的加速度
Dat_H = MPU6050_Read_Given_address(MPU6050_ACCEL_YOUT_H);
Dat_L = MPU6050_Read_Given_address(MPU6050_ACCEL_YOUT_L);
*AccY = (Dat_H<<8) | Dat_L;
//Z轴的加速度
Dat_H = MPU6050_Read_Given_address(MPU6050_ACCEL_ZOUT_H);
Dat_L = MPU6050_Read_Given_address(MPU6050_ACCEL_ZOUT_L);
*AccZ = (Dat_H<<8) | Dat_L;
//X轴的重力
Dat_H = MPU6050_Read_Given_address(MPU6050_GYRO_XOUT_H);
Dat_L = MPU6050_Read_Given_address(MPU6050_GYRO_XOUT_L);
*GyroX = (Dat_H<<8) | Dat_L;
//Y轴的重力
Dat_H = MPU6050_Read_Given_address(MPU6050_GYRO_YOUT_H);
Dat_L = MPU6050_Read_Given_address(MPU6050_GYRO_YOUT_L);
*GyroY = (Dat_H<<8) | Dat_L;
//Z轴的重力
Dat_H = MPU6050_Read_Given_address(MPU6050_GYRO_ZOUT_H);
Dat_L = MPU6050_Read_Given_address(MPU6050_GYRO_ZOUT_L);
*GyroZ = (Dat_H<<8) | Dat_L;
}
主函数:
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "MPU6050.h"
int16_t AccX, AccY, AccZ, GyroX, GyroY, GyroZ;
int main(void)
{
OLED_Init();
// OLED_ShowString(0,0,"hello world!",OLED_8X16);
MPU6050_Init();
while (1)
{
MPU6050_Get_Dat(&AccX,&AccY,&AccZ,&GyroX,&GyroY,&GyroZ);
OLED_ShowSignedNum(0,0,AccX,5,OLED_8X16);
OLED_ShowSignedNum(64,0,GyroX,5,OLED_8X16);
OLED_ShowSignedNum(0,16,AccY,5,OLED_8X16);
OLED_ShowSignedNum(64,16,GyroY,5,OLED_8X16);
OLED_ShowSignedNum(0,32,AccZ,5,OLED_8X16);
OLED_ShowSignedNum(64,32,GyroZ,5,OLED_8X16);
OLED_Update();
}
}
硬件IIC控制MPU6050
注意点:
1:首先就是硬件IIC的引脚不能够自己定义,得使用固定的引脚
2:两个引脚都要使用复用模式;也就是复用开漏输出模式,当然它说的是输出模式,但是它的输入并没有被关闭,也是能输入的;
主机发送:时序图

void MPU6050_Write_Given_address(uint16_t address, uint8_t Data)//指定地址写
{
// IIC_Start();
// IIC_send_Byte(MPU6050_address);
// IIC_Receive_ACK();
// IIC_send_Byte(address);
// IIC_Receive_ACK();
// IIC_send_Byte(Data);
// IIC_Receive_ACK();
// IIC_stop();
I2C_GenerateSTART(I2C2, ENABLE);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);//等待EV5标志位
I2C_Send7bitAddress(I2C2, MPU6050_address, I2C_Direction_Transmitter);
// I2C_AcknowledgeConfig(I2C2, ENABLE); //应答并不像软件那样需要手动接收发送,如果程序错误的话,就会通过标志位显示出来
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);//等待EV6标志位
I2C_SendData(I2C2, address);//时序图里面是发送数据,但是这个数据在这里就是内存地址
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);//等待EV8标志位
I2C_SendData(I2C2, Data);//发送数据
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);//等待EV8-2标志位
I2C_GenerateSTOP(I2C2, ENABLE);
}
软件的时序
1:开始
2:发送设备地址+读标志位
3:主机接收应答,(表示从机有应答)
4:主机直接读取数据
5:主机发送应答(表示收到数据)
6:停止
硬件的时序;
1:开始函数
1.1:等待EV5标志位
2:发送设备地址+发标志位
2.1:等待EV6标志位
3:发送设备内存地址
3.1:等待EV8标志位
4:发送数据
4.1:等待EV8-2标志位
5:停止函数
主机接收:时序图
注意:
如果读取的是一个字节的话,那就是要在接收字节之前就ACK给1,在stop停止,不然在读取数据之后就置ACK1,stop的话,由于数据后面紧接着一个ACK0,提示后面继续接收,就会出现多一个字节的情况:
多字节的话,也就是说在最后一个字节的前面提前把ACk置1,stop;
uint8_t MPU6050_Read_Given_address(uint16_t address)//指定地址读
{
uint8_t Data = 0;
// IIC_Start();
// IIC_send_Byte(MPU6050_address);
// IIC_Receive_ACK();
// IIC_send_Byte(address);
// IIC_Receive_ACK();
// IIC_Start();
// IIC_send_Byte(MPU6050_address|0x01);
// IIC_Receive_ACK();
// Data = IIC_Receive_Byte();
// IIC_Send_Ack(1);
// IIC_stop();
I2C_GenerateSTART(I2C2, ENABLE);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
I2C_Send7bitAddress(I2C2, MPU6050_address, I2C_Direction_Transmitter);
// I2C_AcknowledgeConfig(I2C2, ENABLE); //应答并不像软件那样需要手动接收发送,如果程序错误的话,就会通过标志位显示出来
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
I2C_SendData(I2C2, address);//时序图里面是发送数据,但是这个数据在这里就是内存地址
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
I2C_GenerateSTART(I2C2, ENABLE);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
I2C_Send7bitAddress(I2C2, MPU6050_address, I2C_Direction_Receiver);//接收
// I2C_AcknowledgeConfig(I2C2, ENABLE); //应答并不像软件那样需要手动接收发送,如果程序错误的话,就会通过标志位显示出来
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) == ERROR);
I2C_AcknowledgeConfig(I2C2, DISABLE);//置DISABLE就是ACK给1,让从机不要发送了,这里的ACk标志位好像跟前面软件的ACK不一样刚好反过来;
I2C_GenerateSTOP(I2C2, ENABLE);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
Data = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2, ENABLE);
return Data;
}
软件时序:
1:开始
2:主机发送设备地址+写标志位
3:接收应答(表示从机有应答)
4:主机发送设备内部地址
5:主机接收应答(表示从机接收到了地址数据,从机有应答)
6:开始;
7:主机再次发送设备地址+读标志位
8:主机接收应答(表示从机有应答)
9:主机读取数据:
10:主机发送应答1(表示主机接收到了数据,但是不想从机再发数据了,如果是发应答0,那就是说从机继续给数据)
11:停止
硬件时序:
1:开始函数
1.1:等待EV5标志位
2:发送设备地址+发标志位
2.1:等待EV6标志位
3:发送设备内存地址
3.1:等待EV8标志位
4:开始函数
4.1:等待EV5标志位
5:发送设备地址+读标志位
5.1:等待EV6标志位
6:主机发送应答DISABLE
7:停止
8:等待EV7标志位
8.1:读取数据
9:主机发送应答ENABLE
SPI通信
SPI简介:

CS对应SS:是从机选择线 ,对主机来说的输出线
CLK对应SCK:是时钟线,对主机来说是输出线
DI对应MOSI:是主机的输出线,从机的输入线,
DO对应MISO:是主机的输入线,从机的输出线
SPI基本时序
起始条件与终止条件:

void zz_SPI_Write_SS(uint8_t Bitvalue)//控制SS从机选择线
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)Bitvalue);
}
void zz_SPI_Write_SCK(uint8_t Bitvalue)//时钟控制函数
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)Bitvalue);
}
void zz_SPI_Write_MOSI(uint8_t Bitvalue)//主机输出数据线
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)Bitvalue);
}
uint8_t zz_SPI_Read_MISO(void)//主机输入数据线
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
void zz_SPI_Start(void)//SS从1到0,时序开始
{
zz_SPI_Write_SS(0);
}
void zz_SPI_Stop(void)//SS从0到1,时序结束
{
zz_SPI_Write_SS(1);
}
交换一个字节,模式0

软件的交换一个字节:先不管SS从机选择线,
1:首先由上面的图可以看出来,在SCK第一个边沿来临的之前,主机就把数据的高位放到了输出线MOSI的高位,在这个时刻MISO输入线是没有数据传入的,也不是说没有数据,就是说输入线的数据并不是主机要的数据,所以就不用接收数据,所以,第一步就是在SCK第一个边沿(上升沿)来临之际,就应该主机把数据最高位放到数据输出线上
2:拉高SCK,这个时候就是SCK的第一个边沿,下面开始移入数据
3:接着就到了SCK第一个边沿的时候,依照上面图中所说,就是要开始移入数据了
4:拉低SCK,
最后循环8次,这个就完成了主机发送一个字节,同时接收了从机的一个字节,
uint8_t zz_SPI_Swap_Byte(uint8_t Byte) //交换一个字节
{
uint8_t Receive_Byte=0;
uint8_t i=0;
for(i=0; i<8; i++)
{
zz_SPI_Write_MOSI(Byte&(0x80>>i));//发送一位
zz_SPI_Write_SCK(1);
if(zz_SPI_Read_MISO() == 1)//接收一位
{
Receive_Byte|=(0x80>>i);
}
zz_SPI_Write_SCK(0);
}
return Receive_Byte;
}
交换一个字节,模式1

交换一个字节,模式2

交换一个字节,模式3

W25Q64简介



最小擦除单元:一个扇区;
指令集与ID号:




软件SPI-W25Q64
W25Q64指令
#define W25Q64_WRITE_ENABLE 0x06
#define W25Q64_WRITE_DISABLE 0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#define W25Q64_WRITE_STATUS_REGISTER 0x01
#define W25Q64_PAGE_PROGRAM 0x02
#define W25Q64_QUAD_PAGE_PROGRAM 0x32
#define W25Q64_BLOCK_ERASE_64KB 0xD8
#define W25Q64_BLOCK_ERASE_32KB 0x52
#define W25Q64_SECTOR_ERASE_4KB 0x20
#define W25Q64_CHIP_ERASE 0xC7
#define W25Q64_ERASE_SUSPEND 0x75
#define W25Q64_ERASE_RESUME 0x7A
#define W25Q64_POWER_DOWN 0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90
#define W25Q64_READ_UNIQUE_ID 0x4B
#define W25Q64_JEDEC_ID 0x9F
#define W25Q64_READ_DATA 0x03
#define W25Q64_FAST_READ 0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q64_FAST_READ_DUAL_IO 0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q64_FAST_READ_QUAD_IO 0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3
#define W25Q64_DUMMY_BYTE 0xFF
读取ID号
void W25Q64_JEDEC_DID_MID(uint8_t *MID, uint16_t *DID)//获取厂商MID号和设备DID号
{
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_JEDEC_ID);//发送获取ID指令
*MID = zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE);
*DID = zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE);
*DID <<= 8;
*DID |= zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE);
zz_SPI_Stop();
}
写使能
写入操作之前,必须进行写使能,所以写入操作有页擦除,页编程 (指定地址写),
void W25Q64_Write_ENABLE(void)//写使能
{
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_WRITE_ENABLE);
zz_SPI_Stop();
}
读取状态寄存器,等待Busy位过去
我们选择事后等待,所以在写操作之后就等待Busy位过去,,写入操作有页擦除,页编程 (指定地址写),
void W25Q64_READ_STATUS_REGISTER_1_Busy(void)//查看状态寄存器的Busy位,这个函数能确保Busy位过去
{
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_READ_STATUS_REGISTER_1);
while((zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE)&0x01) == 1);//状态寄存器的最低位是Busy位
zz_SPI_Stop();
}
页编程 (指定地址存数据)
最多一次只能存储256个数据
void W25Q64_READ_data(uint32_t address, uint8_t *ARR_Byte, uint16_t count)//指定地址读
{
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_READ_DATA);
zz_SPI_Swap_Byte(address>>16);
zz_SPI_Swap_Byte(address>>8);
zz_SPI_Swap_Byte(address);
for(uint16_t i = 0; i<count; i++)
{
ARR_Byte[i] = zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE);
}
}
页擦除(扇区擦除)
void W25Q64_SECTOR_ERASE(uint32_t address) //页擦除
{
W25Q64_Write_ENABLE();
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_SECTOR_ERASE_4KB);
zz_SPI_Swap_Byte(address>>16);
zz_SPI_Swap_Byte(address>>8);
zz_SPI_Swap_Byte(address);
zz_SPI_Stop();
W25Q64_READ_STATUS_REGISTER_1_Busy();
}
读取数据
void W25Q64_READ_data(uint32_t address, uint8_t *ARR_Byte, uint16_t count)//指定地址读
{
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_READ_DATA);
zz_SPI_Swap_Byte(address>>16);
zz_SPI_Swap_Byte(address>>8);
zz_SPI_Swap_Byte(address);
for(uint16_t i = 0; i<count; i++)
{
ARR_Byte[i] = zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE);
}
}
硬件SPI-W25Q64
SPI简化基本时序


如果对性能没有特别的要求的话,就推荐用非连续传输 ,易封装。如果是连续模式不易于封装。

初始化配置:
CPOL参数:设置的是SCK空闲时的时钟极性,一般选择LOW,空闲时为低电平;
SPI_CRCPolynomial:CRC检验位的多项式,一般不用的话,就填默认值7;
SPI_FirstBit = SPI_FirstBit_MSB://设置为高位先行还是低位先行,我们选择高位先行.我们一般设置为8位数据,高位先行;
发送一个字节的步骤
1:等待TXE标志位为1;
2:发送数据
3:等待RXNE标志位为1,因为发送的接收是同步的,RXEN是接收完成标志位,接收完成了,也就是说发送也完成了,
4:接收交换过来的数据,因为上面已经等待RXEN为1了,已经表明了数据已经存储在RDR寄存器中,所以直接接收就好了;
引脚选择及设置
1:SS从机选择线可以使用软件来软件模拟,(会有函数决定是软件SS还是硬件SS)
2:但是SCK,MISO,MOSI就必须要遵循引脚定义表上的绑定
3:如果选择的是SPI2,而且还要SPI2的引脚复用到其他的引脚的话,那么就要注意解除调试端口功能JT,
4:SS从机选择线:使用的是软件模拟的时序,所以我们使用的还是推挽输出
5:SCK时钟线:因为是硬件的片上外设,所以就是设置为复用推挽输出
6:MOSI主机输出线:因为是硬件的片上外设,所以就是设置为复用推挽输出
7:MISO主机输入线:设置为上拉输入,输入就不存在说是复用还是不复用的问题

代码程序
首先对于基于硬件SPI的W25Q64,第一步应建立一个硬件SPI的函数,再基于硬件SPI建立一个W25Q64的函数,这个样子实现了函数的模块化,便于管理,同时一个很方便的点是只要更改硬件SPI函数的模块化函数就能够实现软硬件切换,所以基于硬件SPI的W25Q64也是基于软件PSI的W25Q64的,
硬件SPI模块函数(SS从机选择线使用软件实现)
#include "stm32f10x.h" // Device header
void zz_SPI_Write_SS(uint8_t Bitvalue)//控制SS从机选择线
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)Bitvalue);
}
void zz_SPI_Init(void)
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
//初始化GPIO
GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Initstructure.GPIO_Pin = GPIO_Pin_4; //SS从机选择线
GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_Initstructure);
GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Initstructure.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_7; //PA5:SCK时钟线 PA7:MOSI主机输出线
GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_Initstructure);
GPIO_Initstructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Initstructure.GPIO_Pin = GPIO_Pin_6; //PA6:MISO主机输入线
GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_Initstructure);
//配置SPI参数
SPI_InitTypeDef SPI_Initstructure;
SPI_Initstructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
SPI_Initstructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_Initstructure.SPI_CPOL = SPI_CPOL_Low;
SPI_Initstructure.SPI_CRCPolynomial = 7; //CRC校验的多项式,我们不用,所以填默认值7
SPI_Initstructure.SPI_DataSize = SPI_DataSize_8b;
SPI_Initstructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线全双工
SPI_Initstructure.SPI_FirstBit = SPI_FirstBit_MSB;//设置为高位先行还是低位先行,我们选择高位先行
SPI_Initstructure.SPI_Mode = SPI_Mode_Master;
SPI_Initstructure.SPI_NSS = SPI_NSS_Soft;
SPI_Init(SPI1, &SPI_Initstructure);
//使能SPI
SPI_Cmd(SPI1, ENABLE);
//全部从机不响应
zz_SPI_Write_SS(1);
}
void zz_SPI_Start(void)//SS从1到0,时序开始
{
zz_SPI_Write_SS(0);
}
void zz_SPI_Stop(void)//SS从0到1,时序结束
{
zz_SPI_Write_SS(1);
}
uint8_t zz_SPI_Swap_Byte(uint8_t Byte) //交换一个字节
{
//等待TXE标志位为1 ,标志位1代表DR发送寄存器为空
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE)==RESET);
//发送数据
SPI_I2S_SendData(SPI1, Byte);
//等待RXNE标志位为1,为1代表就是数据发送完成,同时RDR接收寄存器接收到了数据
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE)==RESET);
//接收数据
return SPI_I2S_ReceiveData(SPI1);
}
基于硬件SPI的W25Q64的模块函数
其实跟软件SPI的W25Q64函数是同一个函数
#include "stm32f10x.h" // Device header
#include "zz_SPI.h"
void W25Q64_READ_STATUS_REGISTER_1_Busy(void);//查看状态寄存器的Busy位,这个函数能确保Busy位过去
#define W25Q64_WRITE_ENABLE 0x06
#define W25Q64_WRITE_DISABLE 0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#define W25Q64_WRITE_STATUS_REGISTER 0x01
#define W25Q64_PAGE_PROGRAM 0x02
#define W25Q64_QUAD_PAGE_PROGRAM 0x32
#define W25Q64_BLOCK_ERASE_64KB 0xD8
#define W25Q64_BLOCK_ERASE_32KB 0x52
#define W25Q64_SECTOR_ERASE_4KB 0x20
#define W25Q64_CHIP_ERASE 0xC7
#define W25Q64_ERASE_SUSPEND 0x75
#define W25Q64_ERASE_RESUME 0x7A
#define W25Q64_POWER_DOWN 0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90
#define W25Q64_READ_UNIQUE_ID 0x4B
#define W25Q64_JEDEC_ID 0x9F
#define W25Q64_READ_DATA 0x03
#define W25Q64_FAST_READ 0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q64_FAST_READ_DUAL_IO 0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q64_FAST_READ_QUAD_IO 0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3
#define W25Q64_DUMMY_BYTE 0xFF
void W25Q64_Init(void)
{
zz_SPI_Init();
}
void W25Q64_Write_ENABLE(void)//写使能
{
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_WRITE_ENABLE);
zz_SPI_Stop();
}
void W25Q64_JEDEC_DID_MID(uint8_t *MID, uint16_t *DID)//获取厂商MID号和设备DID号
{
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_JEDEC_ID);
*MID = zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE);
*DID = zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE);
*DID <<= 8;
*DID |= zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE);
zz_SPI_Stop();
}
void W25Q64_Page_program(uint32_t address, uint8_t *ARR_Byte, uint16_t count)//指定地址写
{
uint16_t i =0;
W25Q64_Write_ENABLE();
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_PAGE_PROGRAM);
zz_SPI_Swap_Byte(address>>16);
zz_SPI_Swap_Byte(address>>8);
zz_SPI_Swap_Byte(address);
for(i=0; i<count; i++)
{
zz_SPI_Swap_Byte(ARR_Byte[i]);
}
zz_SPI_Stop();
W25Q64_READ_STATUS_REGISTER_1_Busy();
}
void W25Q64_SECTOR_ERASE(uint32_t address) //页擦除
{
W25Q64_Write_ENABLE();
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_SECTOR_ERASE_4KB);
zz_SPI_Swap_Byte(address>>16);
zz_SPI_Swap_Byte(address>>8);
zz_SPI_Swap_Byte(address);
zz_SPI_Stop();
W25Q64_READ_STATUS_REGISTER_1_Busy();
}
void W25Q64_READ_STATUS_REGISTER_1_Busy(void)//查看状态寄存器的Busy位,这个函数能确保Busy位过去
{
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_READ_STATUS_REGISTER_1);
while((zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE)&0x01) == 1);//状态寄存器的最低位是Busy位
zz_SPI_Stop();
}
void W25Q64_READ_data(uint32_t address, uint8_t *ARR_Byte, uint16_t count)//指定地址读
{
zz_SPI_Start();
zz_SPI_Swap_Byte(W25Q64_READ_DATA);
zz_SPI_Swap_Byte(address>>16);
zz_SPI_Swap_Byte(address>>8);
zz_SPI_Swap_Byte(address);
for(uint16_t i = 0; i<count; i++)
{
ARR_Byte[i] = zz_SPI_Swap_Byte(W25Q64_DUMMY_BYTE);
}
}
RTC实时时钟
Unix时间戳
概念



BKP备份寄存器
BKP简介与基本结构

TAMPER引脚:当设备需要保障安全时,就可以通过这个引脚,只要这个引脚出现了被设置好了的电平变化,设备就会觉得有危险入侵,就会主动的清除BKP备份数据寄存器里面的数据,

写入和读取BKP备份数据寄存器

简单代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
int main(void)
{
OLED_Init();
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);//使能BKP时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);//使能电源控制器时钟
PWR_BackupAccessCmd(ENABLE);//设置PWR的DBP位,使能对BKP和RTC的访问
BKP_WriteBackupRegister(BKP_DR1, 0x1234);//写寄存器,BKP_DRx:这个DR寄存器中容量的F103只有1~10,大容量型的才有到1~42
while (1)
{
OLED_ShowHexNum(1,1,BKP_ReadBackupRegister(BKP_DR1),4);//读DR寄存器
}
}
RTC实时时钟
简介




硬件电路

RTC实时时钟代码部分
初始化步骤:
1:打开PWR电源控制器的时钟;(在RCC文件)
2:打开BKP备份寄存器的时钟;(在RCC文件)
3:设置PWR_CR的DBP位,使能对PWR与BKP的访问;
(在PER文件的PWR_BackupAccessCmd(FunctionalState NewState);)
4:打开外部低速时钟:(在RCC文件)
5:等待外部低速时钟开启完成(等待LSERDY位为1)

6:选择时钟源(一般选择RTC专用外部低速时钟LSE)
(函数是RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);)
7:使能时钟(使能上面第六步选择的时钟源)
8:等待同步;(等待RTC外部低速时钟和内部写入时钟同步)
9:等待上一步操作完成;
10:配置预分频器(对上面选择的LSE进行分频得到1Hz的频率,参数一般是32768-1);
(进入配置模式,但是不用我们自己写,再配置预分频器的时候就自动进入配置模式里了)
11:等待上一步操作完成;
12:配置计时寄存器(也就是输入一个时间戳)
13;等待上一步操作完成;
注意:1:如果外部低速晶振不起振的话,那就要使用内部低速晶振,对应的参数也要改
2:每个写入操作之后,都要进行等待操作完成
对stdio.h文件
flash闪存
简介

全擦除

页擦除

写入半字

更多推荐




所有评论(0)