STM32万字学习笔记
前言:无意发现之前学STM32做的笔记。。。有点感慨。。有些内容不全。各位自行查找。整体知识量很大
基础知识
单片机
单片机(MCU)的组成:
CPU:处理指令和控制单片机的操作。
内存:包括随机访问存储器(RAM)和只读存储器(ROM),用于存储程序代码和运行时数据。
输入输出接口(I/O):允许单片机与外部世界(如传感器、显示屏、按键等)进行通信。
外设:如定时器、串行通讯接口(如USART、SPI、I2C等)、模数转换器(ADC)、数模转换器(DAC)等。
处理器核心(CPU):
是一个CPU核心,设计由ARM公司提供。它是一种高性能的处理器核心,专为低成本、低功耗的嵌入式应用设计,提供了丰富的指令集和高效的中断处理能力。在这个上下文中,Cortex-M3是CPU。
操作系统
CMSIS-RTOS
定义:CMSIS-RTOS是Cortex Microcontroller Software Interface Standard(CMSIS)的一部分,由ARM公司提出。它定义了一个与硬件无关的RTOS接口标准,旨在提高应用程序的可移植性和可重用性。CMSIS-RTOS是一个RTOS API规范,而不是RTOS本身。
目的:主要目的是为了在不同的RTOS实现之间提供一个统一的接口,使得基于Cortex-M系列处理器的应用程序能够更容易地在不同RTOS之间迁移。
实现:多个RTOS提供了CMSIS-RTOS API的实现,包括FreeRTOS、RTX(由ARM提供的RTOS)、以及其他第三方RTOS。
FreeRTOS
定义:FreeRTOS是一个开源的实时操作系统,针对嵌入式设备设计。它提供了多任务处理、时间管理、同步机制等实时操作系统的基本功能。
目的:提供一个轻量级、简单、易于使用的RTOS,用于管理复杂的嵌入式应用中的多任务和时间。FreeRTOS通过其API直接提供这些功能,而不仅仅是一个接口规范。
实现:FreeRTOS本身就是一个完整的RTOS实现,它也提供了符合CMSIS-RTOS API规范的包装层(Wrapper),使得基于FreeRTOS的应用程序能够使用CMSIS-RTOS API。
堆 heap
含义:就是一块空闲的内存,需要提供管理函数
malloc:从堆里划出一块空间给程序使用
free:用完后,再把它标记为"空闲"的,可以再次使用
栈 stack
函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
可以从堆中分配一块空间用作栈

钩子函数(Hook Functions)
钩子函数提供了一种机制,允许开发者在RTOS的关键点插入自己的函数或处理逻辑,例如在每个tick周期、任务切换、空闲任务运行时执行用户定义的代码。这些函数允许开发者监控RTOS的运行状态、收集统计信息或添加自定义的行为。
Delay函数
标准库
static u8 fac_us=0; //us延时倍乘数
static u16 fac_ms=0; //ms延时倍乘数,在ucos下,代表每个节拍的ms数
/****************************************************************************
* 名 称: delay_init()
* 功 能:延时函数初始化
* 入口参数:无
* 返回参数:无
* 说 明:
****************************************************************************/
void delay_init()
{
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
fac_us=SYSCLK/8;
fac_ms=(u16)fac_us*1000; //每个ms需要的systick时钟数
}
/****************************************************************************
* 名 称: void delay_us(u32 nus)
* 功 能:延时nus
* 入口参数:要延时的微妙数
* 返回参数:无
* 说 明:nus的值,不要大于798915us
****************************************************************************/
void delay_us(u32 nus)
{
u32 midtime;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
midtime=SysTick->CTRL;
}
while((midtime&0x01)&&!(midtime&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
/****************************************************************************
* 名 称: void delay_xms(u16 nms)
* 功 能:延时nms
* 入口参数:要延时的毫妙数
* 返回参数:无
* 说 明:SysTick->LOAD为24位寄存器,所以,最大延时为: nms<=0xffffff*8*1000/SYSCLK
对168M条件下,nms<=798ms
****************************************************************************/
void delay_xms(u16 nms)
{
u32 midtime;
SysTick->LOAD=(u32)nms*fac_ms;//时间加载(SysTick->LOAD为24bit)
SysTick->VAL =0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
midtime=SysTick->CTRL;
}
while((midtime&0x01)&&!(midtime&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
/****************************************************************************
* 名 称: void delay_ms(u16 nms)
* 功 能:延时nms
* 入口参数:要延时的毫妙数
* 返回参数:无
* 说 明:nms:0~65535
****************************************************************************/
void delay_ms(u16 nms)
{
u8 repeat=nms/540; //这里用540,是考虑到某些客户可能超频使用,
//比如超频到248M的时候,delay_xms最大只能延时541ms左右了
u16 remain=nms%540;
while(repeat)
{
delay_xms(540);
repeat--;
}
if(remain)delay_xms(remain);
}
HAL库
/****************************************************************************
* 名 称: void delay_init(uint16 sysclk)
* 功 能:delay初始化
* 入口参数:sysclk 系统时钟 单位M
* 返回参数:无
* 说 明:nus的值,不要大于798915us
****************************************************************************/
void delay_init(uint16 sysclk)
{
SysTick->CTRL = 0; /*滴答定时器的状态控制寄存器 清零 目的清空HAL_Init自动配置的参数*/
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);
fac_us=SYSCLK/8; /*结合上面代码 得到真正计数频率 获得1us的时基 即1us数fac_us次*/
}
/****************************************************************************
* 名 称: void delay_us(u32 nus)
* 功 能:延时nus
* 入口参数:要延时的微妙数
* 返回参数:无
* 说 明:nus的值,不要大于798915us
****************************************************************************/
void delay_us(u32 nus)
{
u32 midtime;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
midtime=SysTick->CTRL;
}
while((midtime&0x01)&&!(midtime&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
/****************************************************************************
* 名 称: void delay_ms(u16 nms)
* 功 能:延时nms
* 入口参数:要延时的毫妙数
* 返回参数:无
* 说 明:
****************************************************************************/
void delay_ms(u16 nms)
{
u32 i;
for(i=0;i<nms;i++) delay_us(1000);
}
引脚重映射
STM32的引脚可设置为可设置为:普通IO功能、复用功能、重映射功能。
普通IO功能: 拉高、拉低 、悬空
复用功能:TIM、USART、IIC
重映射功能:将某些I/O口上面的功能映射到其他I/O口上面去
WDG
分类:窗口看门狗、独立看门狗
| IWDG独立看门狗 | WWDG窗口看门狗 | |
|---|---|---|
| 复位 | 计数器减到0后 | 计数器T[5:0]减到0后、过早重装计数器 |
| 中断 | 无 | 早期唤醒中断 |
| 时钟源 | LSI(40KHz) | PCLK1(36MHz) |
| 预分频系数 | 4、8、32、64、128、256 | 1、2、4、8 |
| 计数器 | 12位 | 6位(有效计数) |
| 超时时间 | 0.1ms~26214.4ms | 113us~58.25ms |
| 喂狗方式 | 写入键寄存器,重装固定值RLR | 直接写入计数器,写多少重装多少 |
| 防误操作 | 键寄存器和写保护 | 无 |
| 用途 | 独立工作,对时间精度要求较低 | 要求看门狗在精确计时窗口起作用 |
调用函数:
void IWDG_WriteAccessCmd(uint16_t IWDG_WriteAccess); void IWDG_SetPrescaler(uint8_t IWDG_Prescaler); void IWDG_SetReload(uint16_t Reload); void IWDG_ReloadCounter(void); void IWDG_Enable(void); FlagStatus IWDG_GetFlagStatus(uint16_t IWDG_FLAG); FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG); void RCC_ClearFlag(void);
GPIO
对于407VET6,一个GPIO的模式需要配置以下三个参数:
GPIO_InitStructure.GPIO_Mode =
GPIO_InitStructure.GPIO_OType
GPIO_InitStructure.GPIO_PuPd = ;
typedef enum
{
GPIO_Mode_IN = 0x00, /*!< GPIO Input Mode */
GPIO_Mode_OUT = 0x01, /*!< GPIO Output Mode */
GPIO_Mode_AF = 0x02, /*!< GPIO Alternate function Mode */
GPIO_Mode_AN = 0x03 /*!< GPIO Analog Mode */
}GPIOMode_TypeDef;
typedef enum
{
GPIO_OType_PP = 0x00, /*推挽模式*/
GPIO_OType_OD = 0x01
}GPIOOType_TypeDef;
typedef enum
{
GPIO_PuPd_NOPULL = 0x00,
GPIO_PuPd_UP = 0x01,
GPIO_PuPd_DOWN = 0x02
}GPIOPuPd_TypeDef;
IIC
简介:一个时钟线SCL、一个数据线SDA
系统滴答定时器
系统滴答定时器优先级与HAL_Delay()的执行有关。
HAL_Delay()只能在主函数或者优先级低于系统滴答定时器的中断中执行
SPI通信
引脚:
MOSI:复用推挽
MISO:上拉输入
SCK:复用推挽
NSS:推挽
SPI_InitTypeDef结构体
SPI_InitStructure.SPI_Mode //模式,指定32是主机(Master)还是从机(Slave) SPI_InitStructure.SPI_Direction //方向,可裁剪引脚 SPI_InitStructure.SPI_DataSize //数据宽度,选择为8位还是16位 一般8位 SPI_InitStructure.SPI_FirstBit //先行位,选择高位先行还是低位先行 一般高位 SPI_InitStructure.SPI_BaudRatePrescaler //波特率分频系数 PCLK/PSC=时钟频率 PCLK由APB线决定 SPI_InitStructure.SPI_CPOL //SPI极性,默认低电平还是高,一般低极性(低电平) SPI_InitStructure.SPI_CPHA //SPI相位,选第一个时钟边沿采样,极性和相位决定选择SPI模式0 SPI_InitStructure.SPI_NSS //NSS,选择由软件控制,可以自己选引脚 SPI_InitStructure.SPI_CRCPolynomial //CRC多项式,暂时用不到,给默认值7 SPI_Init(SPI1, &SPI_InitStructure); //将结构体变量交给SPI_Init,配置SPI1
软件SPI通信核心:交换一个字节
-
准备好写的数据 0或1
-
把SCK拉高
-
瞅一眼接受的是0还是1
-
把SCK拉低
//SPI交换一个字节
uint8_t SPI_SwapByte(uint8_t Byte)
{
uint8_t i,ByteReceive=0x00; //白纸等待往里写数据
for(i=0;i<8;i++)
{
W_MOSI(Byte&(0x80>>i)); //写数据的第i位
W_SCK(1);
if(R_MISO()==1) //对面数据第i位有1
{
ByteReceive=ByteReceive|(0x80>>i); //ByteReceive第i位写个1
}
W_SCK(0);
}
return ByteReceive;
}
NVIC:
NVIC:主要作用就是配置中断优先级的
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup); //用来中断分组 (选定一个NVIC) void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct); //NVIC初始化 void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset); //设置中断向量表 void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState); //低功耗模式配置 void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource); /*NVIC配置*/ NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量 NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //选择配置NVIC的EXTI15_10线 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1 NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
AFIO:
作用:
配置端口复用
配置端口重映射
中断引脚选择
标准库相关函数:
void GPIO_AFIODeInit(void); //清除AFIO的所有配置 void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState); //锁定某个引脚配置 void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource); //AFIO事件输出功能 void GPIO_EventOutputCmd(FunctionalState NewState); void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState); //选择引脚重映射 void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource); //配置外部中断线 void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface); //以太网相关
中断:
按中断源分类:外部中断和内部中断
按中断去向分类:中断相应和事件响应(是触发代码执行还是触发外设动作)
外部配置:
相关函数:
void EXTI_DeInit(void); //清除EXTI的所有配置 void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct); //EXTI初始化 void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct); //给EXTI_InitStruct赋默认值 void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line); //直径让软件给中断线产生一次中断 FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line); //查看中断标志位 主程序里用 void EXTI_ClearFlag(uint32_t EXTI_Line); //清除中断标志位 主程序里用 ITStatus EXTI_GetITStatus(uint32_t EXTI_Line); //查看中断标志位的状态 中断里用 void EXTI_ClearITPendingBit(uint32_t EXTI_Line); //清除中断标志位的状态 中断里用 //差别 上面一组仅仅查看标志位 下面一组是判断中断是否产生
配置过程:开启时钟——> GOIO配置——> AFIO选定 ——>中断配置(外部中断+NVIC分组) ——> 中断函数
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB14引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line14; //选择配置外部中断的14号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式(另一个事件模式)
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
/*即抢占优先级范围:0~3,响应优先级范围:0~3此分组配置在整个工程中仅需调用一次若有多个中断,
可以把此代码放在main函数内,while循环之前,若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置*/
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //选择配置NVIC的EXTI15_10线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/**
* 函 数:EXTI15_10外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET) //判断是否是外部中断14号线触发的中断,在中断中用
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
... //外部中断每产生一次所要执行的内容
}
EXTI_ClearITPendingBit(EXTI_Line14); //清除外部中断14号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
关键:
1,GOIO与边沿触发:
默认高电平 ——> 上拉输入 ——> 下降沿触发
默认低电平 ——> 下拉输入 ——> 上升沿触发
所以,按键一段IO口,一端高电平,则下拉输入, 上升沿触发。反之同理。
2,外部中断线:检测到电平变化便产生中断的引脚 每个数字的IO口只能有一个中断进入 NVIC(从EXTI_InitTypeDef可看出)
3,外部中断的NVIC(NVIC_IRQChannel):
EXTI0_IRQn ——>数字为 0的IO引脚都走这个通道
EXTI1_IRQn ——>数字为 1的IO引脚都走这个通道
EXTI2_IRQn ——>数字为 2的IO引脚都走这个通道
EXTI3_IRQn ——>数字为 3的IO引脚都走这个通道
EXTI4_IRQn ——>数字为 4的IO引脚都走这个通道
EXTI9_5_IRQn ——>数字为 5到 9 的IO引脚都走这个通道
EXTI15_10_IRQn ——>数字为 10到 15 的IO引脚都走这个通道
//部分通道所有F1xx都有,部分依据型号而定
4,中断函数的名字(与中断通道相对应)
EXTI0_IRQHandler EXTI1_IRQHandler EXTI2_IRQHandler EXTI3_IRQHandler EXTI4_IRQHandler EXTI9_5_IRQHandler EXTI15_10_IRQHandler
5,中断模式和时间模式
EXTI_Mode_Interrupt //当外部引脚触发了中断条件时,微控制器会立即跳转到中断服务程序(ISR)执行相应的中断处理程序。 EXTI_Mode_Event //不会立即执行中断服务程序,而是将事件标志设置为待处理状态。
定时器中断: 基本配置:
串口:
串口配置:
/*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOX, ENABLE); //开启GPIOX的时钟 /*GPIO初始化*/ GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA10引脚初始化为上拉输入 /*USART初始化*/ USART_InitTypeDef USART_InitStructure; //定义结构体变量 USART_InitStructure.USART_BaudRate = 9600; //波特率 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要 USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择 USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要 USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位 USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位 USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1 /*中断输出配置*/ USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断 /*NVIC中断分组*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2 /*NVIC配置*/ NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1 NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设 /*USART使能*/ USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
硬件流控制:
作用:当两台设备进行串口通信,假如他们对数据的处理速度不同。如果接收端数据缓冲区已满,则此时继续发送来的数据就会丢失。使用流控机制时,当接收端数据处理能力饱和时,就发出“不再接收”的信号,发送端就停止发送,直到接收端处理能力释放,发送“可以继续发送”的信号给发送端时,发送端才继续发送数据
本质:在串口的基础上加上RTS和CTS来控制数据传输 易用软件模拟
中断配置:
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断
USART_IT_TXE 发送中断 //一般只开启发送和接受中断
USART_IT_RXNE 接收中断
串口重定向
底层函数:
函数名前有__,如
int __io_putchar(int ch) __attribute__((weak));
表明该函数与系统有关,谨慎调用。
定时器
定时器模式:
基础知识:
时基单元: 预分频器、计数器、自动重装器
定时器输出PWM波形:
理解本质:利用输出比较,在定时器计数值大于某一值时,输出1;小于某一值,输出0
所需外设:一个定时器的一个通道可输出一个PWM波
编码盘测速:
所需配置:一个定时器,及其TIMx1、TIMx2两个通道
RTC&BKP&PWR
RTC(实时时钟)
注:以下特征以stm32f407VET6为准
本质:
配置一个频率为1HZ的时钟
需注意和BKP结合使用
需注意和低功耗模式结合使用
核心:
访问存储数据的影子寄存器 可写入读出
BCD码存储数据
由备用电源接口VBAT供电
可连接三个中断:秒中断、溢出中断、闹钟中断
开启PWR,BKP时钟才能使能RTC,BKP访问(f103)
开启PWR时钟才能使能RTC,BKP访问(f403)
进入配置模式才能写 即关闭 闹钟/唤醒中断
软件需等寄存器同步标志被置1才能读
RTC时钟源:
LSI:(Low Speed Internal clock)内部的低速振荡器 32.768KHZ
LSE:(Low Speed External clock):外部低速振荡器,通常连一个32.768 kHz的石英。提供非常稳定的时钟信号,主用于RTC
HSE(High Speed External clock):外部高速振荡器,可连接到一个4 MHz至26 MHz的石英或外部时钟源。可通过PLL进一步倍频
PLL:锁相环
LSE为了省电默认关闭,需要手动开启
配置流程:
-
启PWR,BKP时钟使能RTC的访问(配置时钟需要访问寄存器)
-
启动时钟并选择时钟
-
配置预分频器使得时钟为1HZ
-
写入CNT,相当于给一个初始时间
-
选择配置中断(f1每秒、溢出、闹钟)(f4唤醒中断、溢出、闹钟、入侵检测)
配置时钟:
void RCC_LSEConfig(uint8_t RCC_LSE); /*配置外部低速时钟*/ void RCC_HSEConfig(uint8_t RCC_HSE); /*配置外部高速时钟*/ void RCC_LSICmd(FunctionalState NewState); /*控制内部低速时钟*/ void RCC_HSICmd(FunctionalState NewState); /*使能内部高速时钟*/ void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource); /*选择RTC时钟源*/ void RCC_RTCCLKCmd(FunctionalState NewState); /*使能RTC时钟*/ FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG); /*获得RCC标志位,为1才算启动完成*/
配置RTC
RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟 RTC_InitStructure.RTC_AsynchPrediv = 0x7F; //RTC 异步分频系数(1~0X7F) RTC_InitStructure.RTC_SynchPrediv = 0xFF; //RTC 同步分频系数(0~7FFF) RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24;//24 小时格式 RTC_Init(&RTC_InitStructure);//初始化 RTC 参数
使用RTC
typedef struct
{
uint8_t RTC_WeekDay; /*星期几*/
uint8_t RTC_Month; /*月份*/
uint8_t RTC_Date; /*日期*/
uint8_t RTC_Year; /*年份*/
}RTC_DateTypeDef;
typedef struct
{
uint8_t RTC_Hours; /*小时*/
uint8_t RTC_Minutes;
uint8_t RTC_Seconds;
uint8_t RTC_H12;
}RTC_TimeTypeDef;
ErrorStatus RTC_SetTime(uint32_t RTC_Format, RTC_TimeTypeDef* RTC_TimeStruct); /*设置RTC时间 bin or BCD*/
ErrorStatus RTC_SetDate(uint32_t RTC_Format, RTC_DateTypeDef* RTC_DateStruct); /*设置RTC日期 bin or BCD*/
void RTC_GetTime(uint32_t RTC_Format, RTC_TimeTypeDef* RTC_TimeStruct); /*获取RTC时间 bin or BCD*/
void RTC_GetDate(uint32_t RTC_Format, RTC_DateTypeDef* RTC_DateStruct); /*获取RTC日期 bin or BCD*/
配置闹钟
typedef struct
{
RTC_TimeTypeDef RTC_AlarmTime; /*闹钟时间 */
uint32_t RTC_AlarmMask; /*闹钟时间掩码 */
uint32_t RTC_AlarmDateWeekDaySel; /* */
uint8_t RTC_AlarmDateWeekDay; /*第三个参数决定*/
}RTC_AlarmTypeDef;
void RTC_SetAlarm(uint32_t RTC_Format, uint32_t RTC_Alarm, RTC_AlarmTypeDef* RTC_AlarmStruct);/*设置闹钟*/
void RTC_GetAlarm(uint32_t RTC_Format, uint32_t RTC_Alarm, RTC_AlarmTypeDef* RTC_AlarmStruct);/*读取闹钟*/
ErrorStatus RTC_AlarmCmd(uint32_t RTC_Alarm, FunctionalState NewState); /*闹钟使能*/
配置唤醒中断(F4)
void RTC_Set_WakeUp(void)
{
RTC_WakeUpCmd(DISABLE); //关闭 WAKE UP
RTC_WakeUpClockConfig(RTC_WakeUpClock_CK_SPRE_16bits);//每秒一次中断
RTC_SetWakeUpCounter(0); //WAKE UP计数器值(RTC时钟源频率 / WAKE UP时钟周期) - 1
RTC_ClearITPendingBit(RTC_IT_WUT); //清除 RTC WAKE UP 的标志
EXTI_ClearITPendingBit(EXTI_Line22);//清除 LINE22 上的中断标志位
RTC_ITConfig(RTC_IT_WUT,ENABLE);//开启 WAKE UP 定时器中断
RTC_WakeUpCmd( ENABLE);//开启 WAKE UP 定时器
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line22; //看EXTI库配置
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断模式,执行中断函数
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //上升沿触发(看手册)
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = RTC_WKUP_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;//抢占优先级 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;//响应优先级 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道
NVIC_Init(&NVIC_InitStructure);//配置
}
BKP(备份寄存器)
本质:相当于一个EEPROM
核心:
掉电丢失
由VBAT供电
库函数:
/*f103有专门的文件,f407在RTC文件里*/ /*初始化 在103还需多一步 使能BKP时钟*/ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);//使能 PWR 时钟 PWR_BackupAccessCmd(ENABLE); //使能BKP RTC_Read_Val = RTC_ReadBackupRegister(RTC_BKP_DR0); //读函数 RTC_WriteBackupRegister(RTC_BKP_DR0,RTC_Write_Val); //写函数
低功耗模式
DMA
定义:Direct Memory Access直接存储器访问
目的:绕过CPU传输数据
四种方式:
外设到内存
内存到外设
内存到内存
外设到外设
配置主要参数:
源地址、
目标地址
传输数据量
搬运过程(抽象): 源地址 ——> 一排寄存器
目的地址 ——> 一排寄存器
把源地址寄存器的数值复制到目的寄存器地址数值
四种转运模式模式:
源地址第一个寄存器 ——> 一遍遍复制到目的地址第一个寄存器
源地址第一个寄存器 ——> 一次次按顺序复制到目的地址寄存器
源地址寄存器按顺序 ——> 复制到目的地址第一个寄存器
源地址寄存器按顺序 ——> 复制到目的地址对应的寄存器
转运数据: 类似数据类型强制转换:
低位转高位 ——> 补0
高位转低位 ——> 截断
参数解读(从外设到存储器):
外设配置
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; //外设基地址,给定形参AddrA DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据宽度,选择字节 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //外设地址自增,选择使能
存储器配置
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; //存储器基地址,给定形参AddrB DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度,选择字节 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能
转运配置:
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器 DMA_InitStructure.DMA_BufferSize = Size; //转运的数据大小(转运次数) DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //模式,选择正常模式 DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //存储器到存储器,选择使能 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等 /*DMA_Mode: 正常模式: 计数器到0就停转 循环模式: 计数器到0就自动重装, 数据指针恢复到原来的位置,然后开启下一轮转存 DMA_M2M: 软件触发:以最快速度传输完成 不能和循环模式同时用 否则会停不下来 硬件触发: DMA_Priority: 第一阶段(==软件阶段==):每个通道的优先级可在==DMA_CCRx==寄存器中设置,有四个等级:最高、高、中和低优先级。 第二阶段(硬件阶段):如果两个请求有相同软件优先级,较低编号的通道比较高编号的通道有较高的优先级。 (大容量芯片中,DMA1控制器拥有高于DMA2控制器的优先级。且多个请求通过逻辑或输入到DMA控制器,只能有一个请求有效。)*/
函数解析:
//清楚某一个DMA通道的配置 void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx); //对某一个DMA通道进行初始化 void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct); //给DMA_InitStruct结构体赋一个初始值 void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct); //DMA使能 void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState); /*DMA中断配置 可以在数据传输错误 传输一半 传输完成时产生中断*/ void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState); //写入传输计数器,指定将要转运的次数 写前要对DMA失去能 void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); //DMA通道中剩余的传输单元数 uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx); //DMA工作完成标志位 一般while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG); //清楚DMA工作完成标志位 void DMA_ClearFlag(uint32_t DMAy_FLAG); //查询DMA中断产生情况 (SET or RESET) ITStatus DMA_GetITStatus(uint32_t DMAy_IT); //清楚DMA中断标志位 void DMA_ClearITPendingBit(uint32_t DMAy_IT)
DMA配置:
* 函 数:DMA初始化
* 参 数:AddrA 原数组的首地址
* 参 数:AddrB 目的数组的首地址
* 参 数:Size 转运的数据大小(转运次数)
* 返 回 值:无
*/
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
MyDMA_Size = Size; //将Size写入到全局变量,记住参数Size
/*开启时钟*/
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA的时钟
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据宽度,选择字节
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //外设地址自增,选择使能
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; //存储器基地址,给定形参AddrB
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度,选择字节
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器
DMA_InitStructure.DMA_BufferSize = Size; //转运的数据大小(转运次数)
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //模式,选择正常模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //存储器到存储器,选择使能
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
/*DMA使能*/
DMA_Cmd(DMA1_Channel1, DISABLE); //这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
}
/**
* 函 数:启动DMA数据转运
* 参 数:无
* 返 回 值:无
*/
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE); //DMA失能,在写入传输计数器之前,需要DMA暂停工作
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size); //写入传输计数器,指定将要转运的次数
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能,开始工作
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); //等待DMA工作完成
DMA_ClearFlag(DMA1_FLAG_TC1); //清除工作完成标志位
}
{...
MyDMA_Init( AddrA, AddrB, Size);
MyDMA_Transfer();
}
ADC
四种模式:
单次转换非扫描
连续转换非扫描
单次转换扫描
连续转换扫描
连续转换:转换了一次后,自动开始下一次转换
扫描模式:转换一次后,下一次转换通道自动向下移动一位,否则仅转换当前通道
Vref+参考电压引脚
所需函数
void ADC_DeInit(ADC_TypeDef* ADCx); //ADC清除配置 void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);//ADC初始化 void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct); //ADC结构体初始化 void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState); //ADC启动 void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState); //ADC+DMA启动 void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState); //ADC中断配置 void ADC_ResetCalibration(ADC_TypeDef* ADCx); //复位校准 FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx); //获取复位校准状态 void ADC_StartCalibration(ADC_TypeDef* ADCx); //开始校准 FlagStatus ADC_GetCalibrationStatus(ADC_TypeDef* ADCx); //获取开始校准状态 void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState); //软件启动ADC转换 FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG); //获取ADC转换状态 ,看EOC标志位是否置1 void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number); //配置ADC间断模式,每隔几通道 void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState); //启动间断模式
配置过程:
/*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 /*设置ADC时钟*/ //ADC转换需要时间,频率太高会不稳定,所以需要分频 RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz /*GPIO初始化*/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //配置成专用的模拟输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0引脚初始化为模拟输入 /*规则组通道配置*/ /*分别是采样通道,采样时间,*/ ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //规则组序列1的位置,配置为通道0 /*ADC初始化*/ ADC_InitTypeDef ADC_InitStructure; //定义结构体变量 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐,选择右对齐 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发,不需要外部触发 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //连续转换,失能,每转换一次规则组序列后停止 ADC_InitStructure.ADC_ScanConvMode = DISABLE; //扫描模式,失能,只转换规则组的序列1这一个位置 ADC_InitStructure.ADC_NbrOfChannel = 1; //通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1 ADC_Init(ADC1, &ADC_InitStructure); //将结构体变量交给ADC_Init,配置ADC1 /*ADC使能*/ ADC_Cmd(ADC1, ENABLE); //使能ADC1,ADC开始运行 /*ADC校准*/ ADC_ResetCalibration(ADC1); //固定流程,内部有电路会自动执行校准 复位校准 while (ADC_GetResetCalibrationStatus(ADC1) == SET); //等待复位校准完成 ADC_StartCalibration(ADC1); //开始校准 while (ADC_GetCalibrationStatus(ADC1) == SET); //等待校准完成
读取过程:
/**
* 函 数:获取AD转换的值
* 参 数:无
* 返 回 值:AD转换的值,范围:0~4095
*/
uint16_t AD_GetValue(void)
{
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发AD转换一次
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); //等待EOC标志位,即等待AD转换结束
return ADC_GetConversionValue(ADC1); //读数据寄存器,得到AD转换的结果
}
ADC+DMA
ADC连续转换+DMA循环转运:
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA1的时钟
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
...GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0、PA1、PA2和PA3引脚初始化为模拟输入
/*规则组通道配置*/
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //规则组序列1的位置,配置为通道0
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //规则组序列2的位置,配置为通道1
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //规则组序列3的位置,配置为通道2
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //规则组序列4的位置,配置为通道3
...
ADC_Init(ADC1, &ADC_InitStructure); //扫描模式+连续转换+软件触发
...
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //地址自增+循环模式+ADC触发
/*DMA和ADC使能*/
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA1的通道1使能
ADC_DMACmd(ADC1, ENABLE); //ADC1触发DMA1的信号使能
ADC_Cmd(ADC1, ENABLE); //ADC1使能
... /*ADC校准*/
/*ADC触发*/
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作
DAC
stm32F407VET6有两个DAC外设,
配置过程:
配置GPIO模拟下拉输入
配置DAC模式
设置DAC的值
内存管理
基本概念
堆栈(Stack):简称为栈。一种线性表数据结构,是一种只允许在表的一端进行插入和删除操作的线性表。
核心:
后进先出 (LIFO last in first out)
是一个数据结构的概念
栈区:
核心:
内存小 大小固定 栈的大小是固定的,由操作系统指定
连续内存地址
编译器自动开辟与释放
存放函数参数值,静态变量
堆区:用于分配程序中动态数据结构的内存空间 核心:
内存大 大小不固定 堆空间通常由系统分配初始大小
不连续内存地址
程序员用malloc释放
易失性存储器(掉电丢失)
RAM:随机访问存储器
核心:
临时存储和快速访问
掉电丢失
SRAM:静态随机访问存储器
核心:
读写速度非常快
内存容量小
DRAM:动态随机访问存储器
核心:
读写速度较SRAM慢,但比其他类型的存储器(如硬盘、SSD)快
内存容量大
非易失性存储器(掉电不丢失)
ROM:只读存储器
核心:
存储不需要修改的程序和数据
FLASH:闪存 核心:
速度通常比RAM慢
有限次多次擦写 几千到几万次之间
TF卡,W25Q64属于FLASH
EEPROM:带电可擦 可编程只读存储器
核心:
读写速度:较慢,通常比FLASH慢。
内存小:从几KB到几MB不等,通常用于存储小量数据
擦写次数:有限,但比FLASH多,通常在十万到一百万次之间
AT24C02属于EEPROM
汇编基础:
__align(n)
作用:
用于指定变量或结构体的内存对齐方式。这里的n是一个整数,表示对齐边界,即内存地址应该是n的倍数。
STM32内存分区:
C代码内存分布:
Text段:放程序执行代码的一块内存区域,CPU执行的机器指令
Data段:静态数据区,存放全局变量、静态变量、常量数据
BSS段:通常是指用来存放程序中未初始化的全局变量的一块内存区域。属于静态内存分配。
Heap段:堆,用于存放程序运行中被动态分配的内存段,大小并不固定,可动态扩张或缩减(malloc)。
Stack段:栈,临时创建的局部变量、用来保存/恢复调用现场、可以把堆栈看成一个寄存、交换临时数据的内存区。
内存管理意义:
在单片机中,Malloc()函数会产生内存碎片,自己写内存算法的目的是解决内存碎片这种痛点。
申请内存
DCMI
2.2寸TFT显示屏
基本知识
GPIO 配置之ODR, BSRR, BRR
ODR寄存器可读可写:既能控制管脚为高电平,也能控制管脚为低电平。管脚对于位写1 gpio 管脚为高电平,写 0 为低电平
BSRR 只写寄存器:既能控制管脚为高电平,也能控制管脚为低电平。对寄存器高 16bit 写1 对应管脚为低电平,对寄存器低16bit写1对应管脚为高电平。写 0 ,无动作
BRR 只写寄存器:只能改变管脚状态为低电平,对寄存器 管脚对于位写 1 相应管脚会为低电平。写 0 无动作。
LCD接口分类
| 接口 | 分辨率 | 特性 |
|---|---|---|
| MCU | ≤800*480 | 带SRAM,无需频繁刷新,无需大内存,驱动简单 |
| RGB | ≤1280*800 | 不带SRAM,需要实时刷新,需要大内存,驱动稍微复杂 |
| MIPI | 4K | 不带SRAM,支持分辨率高,省电,大部分手机屏用此接口 |
LCD驱动原理(熟悉)
1,8080时序,LCD驱动芯片一般使用8080时序控制,实现数据写入/读取
2,初始化序列(数组),屏厂提供,用于初始化特定屏幕,不同屏幕厂家不完全相同!
3,画点函数、读点函数(非必需),基于这两个函数可以实现各种绘图功能!
8080时序
并口总线时序,常用于MCU屏驱动IC的访问,由Intel提出,也叫英特尔总线
| 信号 | 名称 | 控制状态 | 作用 |
|---|---|---|---|
| CS | 片选 | 低电平 | 选中器件,低电平有效,先选中,后操作 |
| WR | 写 | ↑ | 写信号,上升沿有效,用于数据/命令写入 |
| RD | 读 | ↑ | 读信号,上升沿有效,用于数据/命令读取 |
| RS | 数据/命令 | 0=命/1=数 | 表示当前是读写数据还是命令,也叫DC信号 |
| D[15:0] | 数据线 | 无 | 双向数据线,可以写入/读取驱动IC数据 |
写数据顺序:
-
设置DC电平,写的是数据还是命令
-
CS引脚拉低 ——> 开始操作
-
WR拉低,准备写数据
-
数据线准备数据 ——> D0~D15赋上对应的值
-
WR拉高 ——>把数据写入芯片
-
CS引脚拉高 ——> 结束操作
-
释放DC电平,退出操作 (感觉DC是WR/RD的一个代称)
读数据顺序
-
设置DC电平
-
CS引脚拉低 ——> 开始操作
-
RD拉低,准备读数据
-
核心步骤:读取寄存器的值给临时变量,在函数结束作为返回值
-
WR拉高,数据结束
-
CS引脚拉高 ——> 结束操作
-
释放DC电平
剥洋葱顺去:DC层(数据还是命令)——> 片选层(选择芯片)——> 操作层(把读写部分包住)——> 核心读写步骤
6条指令即可完成对LCD的基本使用(以9341为例)
| 指令(HEX) | 名称 | 作用 |
|---|---|---|
| 0XD3 | 读ID | 用于读取LCD控制器的ID,区分型号用 |
| 0X36 | 访问控制 | 设置GRAM读写方向,控制显示方向 |
| 0X2A | 列地址 | 一般用于设置X坐标 |
| 0X2B | 页地址 | 一般用于设置Y坐标 |
| 0X2C | 写GRAM | 用于往LCD写GRAM数据 |
| 0X2E | 读GRAM | 用于读取LCD的GRAM数据 |
摄像头
SCCB通信协议(类似IIC)
SCCB(Serial Camera Control Bus)串行摄像头控制总线,由两根线组成:
SIO_C(OV_SCL) 用于传输时钟信号
SIO_D(OV_SDA)用于传输数据信号
起始信号:下降沿
停止信号:上升沿
有效信号:高电平
传输过程:
SCL低电平 ——> 按周期产生高电平信号 ——> 在高电平采集数据
SDA高电平 ——> 产生下降沿开始信号 ——> 在SCL高电平中给出数据 ——> 在第9个周期有一个din‘t位 ——> 在第10个周期产生上升沿停止信号
特点:
三相传输周期
有don’t位
写时序:
| ID Address | X | SUB-Address | X | Write Date | X |
|---|---|---|---|---|---|
| 设备写通信地址 | 内存地址 | 所写数据 |
X:Don’t 位和NA 可写入1或0,对通讯无影响
读时序:
| ID Address | X | SUB-Address | X | ID Address | X | Read Data | X |
|---|---|---|---|---|---|---|---|
| 设备写通信地址 |
注意:不支持连续读写
集成有源晶振12M,无需外部提供时钟
FIFO(first in first out)
本质:对读写寄存器进行了一个扩展
更本质:就是一种先进先出的数据结构
解释:
-
典型串口 读写缓冲寄存器只有一个字节,在发送完一字节数据后,提醒CPU,存入下一字节的数据,然后继续发送下一字节。
-
缺点:每发送一字节数据,都要打断一次CPU,在有操作系统的结构中,还需进行上下文切换,浪费资源
-
做一个扩展,一个存取多个字节(64字节)的数据,全部发送完之后再来提醒CPU存入下一批数据
DCMI(Digital camera interface数字摄像头接口)
| 信号名称 | 信号说明 |
|---|---|
| D[0:13] | 数据(不同数据接口,D有效引脚不同) |
| PIXCLK | 像素时钟(可设置极性) |
| HSYNC | 水平同步/数据有效(行同步信号) |
| VSYNC | 垂直同步(帧同步信号) |
void DCMI_DeInit(void); /* Initialization and Configuration functions *********************************/ void DCMI_Init(DCMI_InitTypeDef* DCMI_InitStruct); void DCMI_StructInit(DCMI_InitTypeDef* DCMI_InitStruct); void DCMI_CROPConfig(DCMI_CROPInitTypeDef* DCMI_CROPInitStruct); void DCMI_CROPCmd(FunctionalState NewState); void DCMI_SetEmbeddedSynchroCodes(DCMI_CodesInitTypeDef* DCMI_CodesInitStruct); void DCMI_JPEGCmd(FunctionalState NewState); /* Image capture functions ****************************************************/ void DCMI_Cmd(FunctionalState NewState); void DCMI_CaptureCmd(FunctionalState NewState); uint32_t DCMI_ReadData(void); /* Interrupts and flags management functions **********************************/ void DCMI_ITConfig(uint16_t DCMI_IT, FunctionalState NewState); FlagStatus DCMI_GetFlagStatus(uint16_t DCMI_FLAG); void DCMI_ClearFlag(uint16_t DCMI_FLAG); ITStatus DCMI_GetITStatus(uint16_t DCMI_IT); void DCMI_ClearITPendingBit(uint16_t DCMI_IT);
引脚解读:
PIXCLK : 作为信号线 周期的高电平信号时采集 数据
HSYNC :高电平有效 即上升沿代表行数据开始传输 在保持高电平时 即是传输此行数据的时间 传输完了恢复低电平(无效)
VSYNC :高电平有效 即上升沿代表一帧开始传输 在保持高电平时 即是传输此帧数据的时间 传输完了恢复低电平(无效)
OV2640
引脚:
| 引脚 | 作用 | 引脚 | 作用 | ||
|---|---|---|---|---|---|
| D0 | 数据传输 | SCL | 时钟线 使用片上I2C外设与它通讯 外设SCL | PB10 | |
| D1 | 数据传输 | SDA | 数据线 外设SCL | PB11 | |
| D2 | 数据传输 | VSYNC | 帧同步信号 | ||
| D3 | 数据传输 | HREF | 行同步信号 | ||
| D4 | 数据传输 | PSCK | 像素时钟信号,一个PCLK时钟,输出一个(或半个)像素 | ||
| D5 | 数据传输 | PWDN | 掉电/省电模式,高电平有效 | ||
| D6 | 数据传输 | RET | 系统复位管脚,低电平有效 随便一个就行 | ||
| D7 | 数据传输 | 3.3 | |||
| NC | 无 | GND |
使用步骤
一、初始化引脚
D0~D7 上拉 上拉输入
RET 系统复位管脚 推挽输出
PWDN 掉电/省电模式 推挽输出
VSYNC 上拉输入 帧同步信号
HREF 上拉输入 行同步信号
PSCK 上拉输入 像素时钟信号
-
开启重映射
-
拉高 PWDN 开启掉电/省电模式
-
复位OV2640 拉低RET10ms 再拉高
二、SCCB配置
SCL 开漏输出 时钟信号
SDA 上拉输入 数据接受
-
操作sensor寄存器
-
软复位OV2640
-
读取厂家ID
-
初始化 OV2640,采用SXGA分辨率(1600*1200)
-
执行初始化序列
#include <stdio.h>
#include <stm32f1xx.h>
#include <string.h>
#include "common.h"
#include "dcmi_ov2640.h" //需要DCMI功能
#define STATE_START 0x01
#define STATE_STOP 0x02
CRC_HandleTypeDef hcrc;
I2C_HandleTypeDef hi2c1;
static uint8_t image_buffer[63716]; // 如果存储空间不够了, 把这个数组改小就行了
static uint8_t image_state; // 图像捕获状态
static uint32_t image_size; // 图像的大小
/* 初始化摄像头 */
static void camera_init(void)
{
GPIO_InitTypeDef gpio;
LL_DMA_InitTypeDef dma;
LL_TIM_InitTypeDef tim;
LL_TIM_IC_InitTypeDef tim_ic;
LL_TIM_OC_InitTypeDef tim_oc;
__HAL_RCC_AFIO_CLK_ENABLE();
__HAL_AFIO_REMAP_I2C1_ENABLE();
__HAL_AFIO_REMAP_TIM3_ENABLE();
__HAL_RCC_CRC_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_I2C1_CLK_ENABLE();
__HAL_RCC_TIM3_CLK_ENABLE();
hcrc.Instance = CRC;
HAL_CRC_Init(&hcrc);
// PB8~9连接摄像头的I2C接口, 设为复用开漏输出
gpio.Mode = GPIO_MODE_AF_OD;
gpio.Pin = GPIO_PIN_8 | GPIO_PIN_9;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &gpio);
// PC3(VSYNC)和PC6(=~(HREF & PCLK))为浮空输入
// PE0~7是摄像头的8位数据引脚, 为浮空输入
// PC5为OV2640的复位引脚, 低电平复位
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_RESET);
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pin = GPIO_PIN_5;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOC, &gpio);
// PC7(XCLK)是OV2640时钟, 由TIM3_CH2提供
// 微雪OV2640摄像头的RET和PWDN引脚默认情况下是没有接到摄像头上的, 无法使用, 这是因为背面有两个0R电阻没有焊
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pin = GPIO_PIN_7;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOC, &gpio);
// 建议把这两个0R电阻焊上去, 使RET和PWDN两个引脚可用
// 否则摄像头一旦死机, 读写不了寄存器了, 就只能板子重新上电才能恢复了
LL_TIM_StructInit(&tim);
tim.Autoreload = 2; // 72MHz/(2+1)=24MHz
tim.Prescaler = 0; // 不分频
LL_TIM_Init(TIM3, &tim);
LL_TIM_OC_StructInit(&tim_oc);
tim_oc.CompareValue = 1; // 决定占空比
tim_oc.OCMode = LL_TIM_OCMODE_PWM2;
tim_oc.OCState = LL_TIM_OCSTATE_ENABLE;
LL_TIM_OC_Init(TIM3, LL_TIM_CHANNEL_CH2, &tim_oc);
LL_TIM_EnableCounter(TIM3); // 打开定时器, 开始输出24MHz XCLK时钟
HAL_Delay(10);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_SET); // 有了XCLK时钟, 才撤销OV2640复位信号
hi2c1.Instance = I2C1;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.ClockSpeed = 10000; // 速率: 10kHz (不可太高, 否则会导致Ack Failure)
HAL_I2C_Init(&hi2c1);
// 每当PC6上出现下降沿时, 就发送一次DMA请求, 采集GPIOC低8位的数据
LL_TIM_IC_StructInit(&tim_ic);
tim_ic.ICActiveInput = LL_TIM_ACTIVEINPUT_DIRECTTI; // PC6(TIM3_CH1)映射到TIM3_CH1上
tim_ic.ICPolarity = LL_TIM_IC_POLARITY_FALLING; // 下降沿触发
LL_TIM_IC_Init(TIM3, LL_TIM_CHANNEL_CH1, &tim_ic);
// 无需让定时器3开始计时, 这里只使用该定时器的一个输入捕获通道
// 配置TIM3_CH1对应的DMA通道
dma.Direction = LL_DMA_DIRECTION_PERIPH_TO_MEMORY;
dma.MemoryOrM2MDstAddress = (uint32_t)image_buffer;
dma.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_BYTE;
dma.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT;
dma.Mode = LL_DMA_MODE_NORMAL;
dma.NbData = sizeof(image_buffer); // 采集的最大图像大小, 超出部分会被自动丢弃!!
dma.PeriphOrM2MSrcAddress = (uint32_t)&GPIOE->IDR;
dma.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_BYTE;
dma.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT;
dma.Priority = LL_DMA_PRIORITY_VERYHIGH;
LL_DMA_Init(DMA1, LL_DMA_CHANNEL_6, &dma);
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_6);
OV2640_Init(JPEG_800x600);
// 打开PC3外部中断
LL_GPIO_AF_SetEXTISource(LL_GPIO_AF_EXTI_PORTC, LL_GPIO_AF_EXTI_LINE3);
LL_EXTI_EnableRisingTrig_0_31(LL_EXTI_LINE_3); // PC3上的上升沿能触发中断
LL_EXTI_EnableFallingTrig_0_31(LL_EXTI_LINE_3); // PC3上的下降沿也能触发中断
LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_3);
HAL_NVIC_EnableIRQ(EXTI3_IRQn); // 允许执行中断服务函数
}
/* 向串口发送图像数据, 并在末尾附上CRC校验码 */
static void dump(const void *data, uint32_t size)
{
uint8_t value;
uint32_t i;
uint32_t temp;
__HAL_CRC_DR_RESET(&hcrc);
for (i = 0; i < size; i++)
{
// 输出图像数据
value = *((uint8_t *)data + i);
printf("%02X", value);
// 每4字节计算一次CRC
if ((i & 3) == 0)
{
if (i + 4 <= size)
temp = HAL_CRC_Accumulate(&hcrc, (uint32_t *)((uint8_t *)data + i), 1);
else
{
temp = 0;
memcpy(&temp, (uint8_t *)data + i, size - i);
temp = HAL_CRC_Accumulate(&hcrc, &temp, 1);
}
}
}
// 输出CRC
temp = (temp >> 24) | ((temp >> 8) & 0xff00) | ((temp & 0xff00) << 8) | ((temp & 0x00ff) << 24);
printf("%08X\n", temp);
}
int main(void)
{
HAL_Init();
clock_init();
usart_init(115200);
printf("STM32F107VC OV2640\n");
printf("SystemCoreClock=%u\n", SystemCoreClock);
camera_init();
while (1)
{
if (image_state == (STATE_START | STATE_STOP))
{
printf("size=%d\n", image_size);
dump(image_buffer, image_size); // 通过串口发送图像, 然后附上CRC校验值
// 让DMA内部指针回到数组的开头
LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_6);
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_6, sizeof(image_buffer));
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_6);
image_state = 0; // 允许采集新图像 (这条语句一次性把START和STOP都清0了)
}
}
}
void EXTI3_IRQHandler(void)
{
LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_3); // 清除中断标志位
if (LL_GPIO_IsInputPinSet(GPIOC, LL_GPIO_PIN_3))
{
// PC3上升沿表示图像数据传输开始
if (image_state != 0)
return; // 如果图像已经开始采集了, 就忽略这个开始信号
image_state = STATE_START;
// 打开TIM3_CH1对应的DMA通道, 开始采集数据
LL_TIM_EnableDMAReq_CC1(TIM3); // 允许PC6上的下降沿触发DMA请求
}
else
{
// PC3下降沿表示图像数据传输结束
if ((image_state & STATE_START) == 0 || (image_state & STATE_STOP))
return; // 忽略没有开始信号的结束信号, 以及重复的结束信号
image_state |= STATE_STOP;
LL_TIM_DisableDMAReq_CC1(TIM3); // 停止采集
image_size = sizeof(image_buffer) - LL_DMA_GetDataLength(DMA1, LL_DMA_CHANNEL_6); // 总量-剩余数据量=图像大小
}
}
1,配置DCMI接口
ESP8266
本质:串口转WIFI
引脚:
RX:接TX
TX:接RX
3v3:接3.3V电压
GND:接地
EN:接3.3V电压
RST:接3.3V电压
核心:发送数据寄存器,
模式:
AP模式:相当于路由器
STA模式:相当于电脑,需要连路由器
STA+AP 模式:两种模式的共存模式
AP:也就是无线接入点,是一个无线网络的创建者,是网络的中心节点。一般家庭或办公室使用的无线路由器就是一个AP。
STA:每一个连接到无线网络中的终端(如笔记本电脑、PDA及其它可以联网的用户设备)都可称为一个站点。
AT指令:
基础部分:
AT:测试能不能正常链接
AT+RST:重启模块
AT+GMR:查看版本信息
2、ATCWMODE=<mode>
功能:
mode=1 : Station模式(接收模式)
mode=2:AP模式(发送模式)
mode=3:AP+Station模式
3、AT+ CWSAP= <ssid>,<pwd>,<chl>, <ecn>
功能:配置AP参数(指令只有在AP模式开启后有效)
ssid:接入点名称
pwd:密码
chl:通道号
ecn:加密方式:(0-OPEN, 1-WEP, 2-WPA_PSK, 3-WPA2_PSK, 4-WPA_WPA2_PSK)
注意:此设置完成后,连接网络会可能出现连接不上的情况,请发送 AT+RST 命令并等待几分钟之 后再连接。
4、AT+CWLIF
功能:查看已接入设备的 IP
5、AT+CIFSR
功能:查看本模块的 IP 地址 注意: AP 模式下无效!会造成死机现象!
6、AT+CWMODE?
功能:查看本机配置模式
7、AT+CIPMUX?
功能:查询本模块是否建立多连接 说明: <mode>:0-单路连接模式, 1-多路连接模式
8、AT+CIPMODE? 功能:查询本模块的传输模式
说明: <mode>:0-非透传模式, 1-透传模式
9、AT+CIPSTO?
功能:查询本模块的服务器超时时间
10、AT+CIPMUX=1
功能:开启多连接模式
11、AT+CIPSERVER=1,8080
功能:创建服务器
关闭 server 服务如下图所示:
说明: <mode>:0-关闭 server 模式, 1-开启 server 模式 <port>:端口号,缺省值为 333
说明: (1) AT+ CIPMUX=1 时才能开启服务器;关闭 server 模式需要重启 (2)开启 server 后自动建立 server 监听,当有 client 接入会自动按顺序占用一个连 接。
12、AT+CIPSTO=2880
功能:设置服务器超时时间
13、AT+CIPSTATUS
功能:查看当前连接
说明: <id>:连接的 id 号 0-4 <type>:字符串参数,类型 TCP 或 UDP <addr>:字符串参数, IP 地址 <port>:端口号 <tetype>: 0-本模块做 client 的连接, 1-本模块做 server 的连接
14、AT+CIPSEND=1,6
功能:向某个连接发送数据
指令: 1)单路连接时(+CIPMUX=0),指令为: AT+CIPSEND=<length> 2)多路连接时(+CIPMUX=1) ,指令为: AT+CIPSEND= <id>,<length> 响应:收到此命令后先换行返回”>”,然后开始接收串口数据 当数据长度满 length 时发送数据。 如果未建立连接或连接被断开,返回 ERROR 如果数据发送成功,返回 SEND OK 说明: <id>:需要用于传输连接的 id 号 <length>:数字参数,表明发送数据的长度,最大长度为 2048
15、AT+CIPSERVER=0 功能:关闭 server 服务
指令: AT+CIPSERVER=<mode>[,<port>] 说明: <mode>:0-关闭 server 模式, 1-开启 server 模式 <port>:端口号,缺省值为 333 响应: OK 说明: (1) AT+ CIPMUX=1 时才能开启服务器;关闭 server 模式需要重启 (2)开启 server 后自动建立 server 监听,当有 client 接入会自动按顺序占用一个连 接。
16、AT+CIPSTART=2,"TCP","192.168.4.101",8080 功能:建立 TCP 连接
指令: 1)单路连接时(+CIPMUX=0),指令为: AT+CIPSTART= <type>,<addr>,<port> 2)多路连接时(+CIPMUX=1),指令为: AT+CIPSTART=<id>,<type>,<addr>,<port> 响应:如果格式正确且连接成功,返回 OK,否则返回 ERROR 如果连接已经存在,返回 ALREAY CONNECT 说明: <id>:0-4,连接的 id 号 <type>:字符串参数,表明连接类型, ”TCP”-建立 tcp 连接, ”UDP”-建立 UDP 连接 <addr>:字符串参数,远程服务器 IP 地址 <port>:远程服务器端口号
17、AT+CIPSEND=2,8
指令: 1)单路连接时(+CIPMUX=0),指令为: AT+CIPSEND=<length> 2)多路连接时(+CIPMUX=1) ,指令为: AT+CIPSEND= <id>,<length> 响应:收到此命令后先换行返回”>”,然后开始接收串口数据 当数据长度满 length 时发送数据。 如果未建立连接或连接被断开,返回 ERROR 如果数据发送成功,返回 SEND OK 说明: <id>:需要用于传输连接的 id 号 <length>:数字参数,表明发送数据的长度,最大长度为 2048 18、AT+CWLAP
功能:查看当前无线路由器列表
响应:正确: (终端返回AP列表)
CWLAP: <ecn>,<ssid>,<rssi> OK 错误: ERROR 说明: < ecn >:0-OPEN, 1-WEP, 2-WPA_PSK, 3-WPA2_PSK, 4-WPA_WPA2_PSK <ssid>:字符串参数,接入点名称 <rssi>:信号强度 19、AT+CWJAP=”MERSAIN”,”XXXXXXXX”
功能:加入当前无线网络
指令: AT+CWJAP=<ssid>,< pwd > 说明: <ssid>:字符串参数,接入点名称 <pwd>:字符串参数,密码,最长64字节ASCII 响应:正确: OK 错误: ERROR 20、AT+CWJAP?
功能:检测是否真的连上该路线网络
指令: AT+CWJAP? 响应:返回当前选择的AP
CWJAP:<ssid> OK 说明: <ssid>:字符串参数,接入点名称 21、AT+CIFSR
功能:查看模块 IP 地址
指令: AT+CIFSR 响应:正确: + CIFSR:<IP address> OK 错误: ERROR 说明: <ssid>:字符串参数,接入点名称
NRF24L01
简介:可以实现点对点或是 1 对 6 的无线通信。 nrf24l01主要用于短距离无线通信,esp8266则主要用于连接互联网。
需要知识基础:stm32单片机、软件模拟SPI通信
PS:本文为了通俗易懂便于理解,可能会失一定专业性,有错误或其他高见欢迎评论区讨论
引脚:
MOSI:主器件数据输出,从器件数据输入
MISO:主器件数据输入,从器件数据输出
SCLK:时钟信号,其上升沿或下降沿是数据移入或移出的条件
CSN :从器件使能信号(片选线)拉低时,NRF响应SPI通信, 拉高时,NRF结束响应SPI通信
CE:CE 协同CONFIG 寄存器共同决定NRF24L01 的状态
IRQ:中断信号线,中断输出。低电平有效,中断时变为低电平,在以下三种情况变低:Tx FIFO 发完并且收到ACK(使能ACK情况下)、Rx FIFO收到数据、达到最大重发次数。
VCC:电压范围1.9V~3.6V,超过3.6V将会烧毁模块。一般电压3.3V左右。除电源VCC和接地端,其余脚都可以直接和普通的5V单片机IO口直接相连,无需电平转换。
GND:地
模式:
Power Down Mode:掉电模式
Tx Mode:发射模式
Rx Mode:接收模式
Standby-1Mode:待机 1 模式
Standby-2 Mode:待机 2 模式
待机模式 待机模式 I 在保证快速启动的同时减少系统平均消耗电流。在待机模式 I 下,晶振正常工作。在待机模式 II 下部分时钟缓冲器处在工作模式。当发送端 TX FIFO 寄存器为空并且 CE 为高电平时进入待机模式 II。在待机模式期间,寄存器配置字内容保持不变。
掉电模式 在掉电模式下,nRF20L01 各功能关闭,保持电流消耗最小。进入掉电模式后,nRF24L01 停止工作,但寄存器内容保持不变。掉电模式由寄存器 PWR_UP 位来控制。
接收模式
接收模式下可以接收6路不同通道的数据,每个数据通道使用不同的地址,但是共用相同的频道。也就是说6个不同的 nRF24L01 设置为发送模式后可以与用一个设置为接收模式的 nRF24L01 进行通讯,而设置为接收模式的 nRF24L01 可以对这个6个发送端进行识别。数据通道0是唯一的一个可以配置为 40 位自身地址的数据通道。1~5数据通道都为8位自身地址和32位公用地址。所有的数据通道都可以设置为增强型 ShockBurst 模式。 nRF24L01 在确认收到数据后记录地址,并以此地址为目标地址发送应答信号。在发送端,数据通道0被用作接收应答信号,因此,数据通道0的接收地址要与发送端地址相等以确保接收到正确的应到信号。
工作过程
按键
基础知识
下文中a为二进制数(0或1)
与(&)
如果两个对应位都是1,结果为1。
如果两个对应位有一个不是1,结果为0。
a^1 = a
a^0 = 0
或(|)
如果两个对应位都是0,结果为0。
如果两个对应位有一个是1,结果为1。
a | 1 = 1
a | 0 = a
非(|)
对变量自身作用,取反
优先级最强,在运算中最先运算
异或(^)
如果两个对应位相同,结果为0。
如果两个对应位不同,结果为1。
a^1 = ~a 与1异或相当于取反
a^0 = a 与0异或相当于不变
优先级(从高到低):
-
按位取反:
~ -
左移位和右移位:
<<和>> -
按位与:
& -
按位异或:
^ -
按位或:
|
状态机代码解析
Key_Val = Key_GetNum();//实时读取键码值
Key_Down = Key_Val & (Key_Old ^ Key_Val);//捕捉按键下降沿
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);//捕捉按键上降沿
Key_Old = Key_Val;//辅助扫描变量
switch(Key_Down)
{
...
}
| 变量值 | 未按下 | 刚按下 | 完全按下 | 刚抬起 | 完全抬起 |
|---|---|---|---|---|---|
| Key_Val | 0 | 1 | 1 | 0 | 0 |
| 第二行代码的Key_Old | 0 | 0 | 1 | 1 | 0 |
| Key_Old ^ Key_Val | 0 | 1 | 0 | 1 | 0 |
| Key_Down | 0 | 1 | 0 | 0 | 0 |
| Key_Up | 0 | 0 | 0 | 1 | 0 |
| 第四行代码的Key_Old | 0 | 1 | 0 | 0 | 0 |
很显然,上述四行代码的意思是:
当轮询时,上一次Key_Val读取为0,这次Key_Val读取为1,视为按下了一次,这次Key_Down为1
当按键完全按下后,即连续两次Key_Val被读取为1之后,Key_Val读取为0,视作抬起一次这次Key_Up为1
当进去完全按下状态后,连续两次检测为0,则可以回到抬起状态
那么,是如何实现消抖的呢?看真值表就知道了
| 变量值 | 刚按下 | 抖动 |
|---|---|---|
| Key_Val | 1 | 0 |
| 第二行代码的Key_Old | 0 | 1 |
| Key_Old ^ Key_Val | 1 | 1 |
| Key_Down | 1 | 0 |
| Key_Up | 0 | 1 |
| 第四行代码的Key_Old | 1 | 0 |
| 变量值 | 刚抬起 | 抖动 |
|---|---|---|
| Key_Val | 0 | 1 |
| 第二行代码的Key_Old | 1 | 0 |
| Key_Old ^ Key_Val | 1 | 1 |
| Key_Down | 0 | 1 |
| Key_Up | 1 | 0 |
| 第四行代码的Key_Old | 0 | 1 |
重点关注 Key_Down 与 Key_Up的值
正常按键状态顺序:
完全抬起
刚按下
完全按下
刚抬起
完全抬起
总结:
完全抬起到刚按下需要一次Key_Val = 1;
刚按下到完全按下需要一次Key_Val = 1;
完全按下到刚抬起需要一次Key_Val = 0;
刚抬起到完全抬起需要一次Key_Val = 0;
若刚按下状态后Key_Val = 0,视作抬起一次(显然不合理,还没进去完全按下,却进去抬起状态了)
若刚抬起状态后Key_Val = 1,视作放下一次(显然不合理,还没进去完全抬起,却又按了一次)
PID算法
C/C++基础
命名空间:
作用:函数名、变量名被框在一个局部空间内作用,防止,重名
性质:
可以不连续
可嵌套
语法:
//空间名字:zhangsan
//成员:a,c,Swap
namespace zhangsan
{
//声明或定义(命名空间成员)
int a;
char c;
void Swap(int* p1, int* p2);
}
//声明语句 引用std空间里的成员cout 进全局作用域(全局命名空间)
using std::cout;
//指示语句 引用std空间里的所有成员 进全局作用域(全局命名空间)
using namespace std;
//不直接引用,使用时引用一下
std::cout << "hello C++" << std::endl;
输入输出
int a
std::cin >> a; //相当于 scanf("",&a);
cout << a << endl; //相当于 printf("%d",a);
函数
//函数缺省(类似python)
int func(int a = 1)
{
return a;
}
//函数重载(参数个数重载,参数类型重载,参数顺序重载,)
int Add(int a, int b)
{
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
int main()
{
Add(1, 3); // 调用第一个Add函数
Add(2.6, 5.7); // 调用第二个Add函数
return 0;
}
new运算符
作用:动态申请存储空间的运算符。
语法:
//使用new申请一个对象 int *p = new int(10);//申请了一个初值为10的整型数据 //使用new申请数组 int *arr = new int[10];//申请了能存放10个整型数据元素的数组,其首地址为arr
new+数据类型(初值),返回值为申请空间的对应数据类型的地址。
delete运算符
new运算符通常搭配delete元素安抚来使用,new用来动态申请存储空间,delete用于释放new申请的空间。 语法格式如下:
delete p; delete[] arr;//注意要删除数组时,需要加[],以表示arr为数组。
new和malloc的区别: 熟悉c语言的都知道,c语言的头文件<malloc.h>中也提供了类似的函数用于动态申请存储空间。那么到底有哪些细节上的差异呢,让我们来细究一下。
new是一个运算符,在标准库函数中就有,不需要导入头文件。malloc函数封装在头文件<malloc.h>中,要使用必须先导入头文件。
使用malloc函数,必须用sizeof运算符给出申请空间的大小。new运算符则会自动计算出所需要申请空间的大小。
malloc返回值通常需要进行强制类型转化。new运算符则可以直接返回对应数据类型的地址。
openmv
00
Lab是由一个亮度通道(channel)和两个颜色通道组成的。在Lab颜色空间中,每个颜色用L、a、b三个数字表示,各个分量的含义是这样的: – L**代表亮度* – **a*代表从绿色到红色的分量 – b**代表从蓝色到黄色**的分量
01 拍摄图像到IDE
import sensor, image, time #五行固定代码 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time = 2000) clock = time.clock() while(True): clock.tick() #初始化时钟对象 img = sensor.snapshot() #拍摄一张照片 print(clock.fps()) #获得拍摄图像的帧率
02 像素统计
sensor.reset() # 初始化摄像头 sensor.set_pixformat(sensor.RGB565) # 格式为 RGB565. sensor.set_framesize(sensor.QVGA) sensor.skip_frames(10) # 跳过10帧,使新设置生效 sensor.set_auto_whitebal(False) #关闭白平衡 ROI=(140,100,40,40) #左上角坐标 宽度 高度 Red = (255,0,0) while(True): img = sensor.snapshot() #拍摄一张照片 img.draw_rectangle(ROI, color=Red)#接下来要统计的区域 画上红色框 statistics=img.get_statistics(roi=ROI) #计算ROI区域的统计信息 color_l=statistics.l_mode() #获取Lab颜色空间中的L(明度)分量的众数 color_a=statistics.a_mode() #获取Lab颜色空间中的A(色度)分量的众数 越大越红绿 color_b=statistics.b_mode() #获取Lab颜色空间中的B(色度)分量的众数 越大越黄蓝 color_RGB = image.lab_to_rgb(color_l,color_a,color_b) #LAB转RGB img.draw_line((0,30,60,30), color=color_RGB,thickness=40)#在左上角显示识别到的颜色 print(color_l,color_a,color_b) img.draw_rectangle(ROI) #在捕获的图像上绘制一个矩形框
03图像上画图
import sensor, image, time
#五行固定代码
#...
clock = time.clock()
line_tuple = (10,10,100,100) //start_x start_y dir_x dir_y
rect_tuple = (100,100,50,50) //start_x start_y width length
Point_x = 150
Point_y = 150
radius = 10
cross_x = 180
cross_y = 180
str_x = 210
str_y = 210
text = "12345qwer"
Red = (255,0,0) #RGB颜色 (0,0,0) 黑色 (255,255,255) 白色
while(True):
clock.tick()
img = sensor.snapshot()
img.draw_line(line_tuple, color=Red)
img.draw_rectangle(rect_tuple, color=Red)
img.draw_circle(Point_x, Point_y, radius, color=Red)
img.draw_cross(cross_x, cross_y, size=5, color=Red)
img.draw_string(str_x, str_y, text, color=Red)
print(clock.fps())
004 找色块并画出
import sensor
import time
#五行固定代码
#...
red = (0, 100, 18, 127, 7, 127) #红色阈值,用工具调整
while True:
clock.tick()
img = sensor.snapshot()
for blobs in img.find_blobs([red]): # 该方法返回的是列表
img.draw_rectangle(blobs.rect())
print(clock.fps())
005串口通信
#关键代码
from machine import UART #引用UART库
#...
uart = UART(3, 19200) #初始化串口3
uart.write("Hello World!\r") #写函数
006NCC模板匹配 很拉
import time
import sensor
import image
from image import SEARCH_EX
sensor.reset() # Set sensor settings
sensor.set_contrast(1)
sensor.set_gainceiling(16) # Max resolution for template matching with SEARCH_EX is QQVGA
sensor.set_framesize(sensor.QQVGA) # You can set windowing to reduce the search image.
# sensor.set_windowing(((640-80)//2, (480-60)//2, 80, 60))
sensor.set_pixformat(sensor.GRAYSCALE)
# Template should be a small (eg. 32x32 pixels) grayscale image.
template1 = image.Image("/1.pgm")
template2 = image.Image("/1.pgm")
template3 = image.Image("/1.pgm")
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot() # find_template(template, threshold, [roi, step, search])
# ROI: The region of interest tuple (x, y, w, h).
# Step: The loop step used (y+=step, x+=step) use a bigger step to make it faster.
# Search is either image.SEARCH_EX for exhaustive search or image.SEARCH_DS for diamond search
# Note1: ROI has to be smaller than the image and bigger than the template.
# Note2: In diamond search, step and ROI are both ignored.
r = img.find_template(template1, 0.70, step=4, search=SEARCH_EX) #roi=(10, 0, 60, 60))
if r:
img.draw_rectangle(r)
r = img.find_template(template2, 0.70, step=4, search=SEARCH_EX) #roi=(10, 0, 60, 60))
if r:
img.draw_rectangle(r)
r = img.find_template(template3, 0.70, step=4, search=SEARCH_EX) #roi=(10, 0, 60, 60))
if r:
img.draw_rectangle(r)
print(clock.fps())
007
##
作者:在下马大胆