目录
1.1. 预备阶段
1.2. 单片机介绍
2. 初识STM32
2.1. STM32
1.1. 预备阶段
1.2. 单片机介绍
1.2.1. 单片机是什么
单片微型计算机(Single Chip Microcomputer)简称为单片机(Microcontrollers),也称为微控制单元(Microcontroller Unit;MCU) ,也就是嵌入式微控制器。单片机是采用集成电路技术,将中央处理器CPU、随机存取存储器RAM(Random Access Memory)、只读存储器ROM(Read-Only Memory)、Flash、多路IO、定时器、计数器、中断系统等设备集成到一块微型硅片上。 可以将单片机看做一个小型且完善的计算机系统。
1.1.3. 应用场景及就业方向
1.1.3.1. 智能设备 单片机工程师 嵌入式工程师
2. 初识STM32
2.1. STM32
2.1.1. 什么是STM32
2.1.2. STM32产品介绍
2.1.3. STM32产品命名规范
2.1.4. STM32与M3的关系
2.1.5. M3与ARM
2.1.6. STM32F0系统架构
系统主要由以下几个模块组成:
两个主模块:
·Cortex-M0 内核及先进高性能总线(AHB bus)
·通用 DMA ( GP-DMA -- general-purpose DMA)
四个从模块:
·内部 FLASH
·内部SRAM
·专门用于连接 GPIO 口的 AHB2
·AHB 到 APB 的桥 , 所有的外设都挂在 APB 总线上
2.2. 开发平台介绍
2.1. 使用的什么芯片
STM32F051K8U6
STM32F051:
ARM Cortex-M0、32位低功耗微处理器、48 MHz 工作频率、64 Kbytes Flash、8 Kbytes SRAM、32 pin
STM32G030C8T6
2.2. NB-IoT硬件平台介绍-开发板
LCD的屏用的是spi协议;平时的大多数的摄像头也是spi协议。
一键还原卡接口就是防止WiFi芯片程序出问题的,这里把图上的NB-IoT换成了WiFi芯片
在线仿真和烧写,看着这么多引脚,其实烧写时起作用的就4个,电源、GND、两个数据传输的。
2.3. 设备各个组成部分
传感器介绍
传感器(英文名称:transducer/sensor)是一种检测装置,能感受到被测量的信息,并能将感受到的信息,按一定规律变换成为电信号或其他所需形式的信息输出,以满足信息的传输、处理、存储、显示、记录和控制等要求。
无人机中的传感器
加速度计
加速度计是用来提供无人机在XYZ三轴方向所承受的加速力。它也能决定无人机在静止状态时的倾斜角度。 当无人机呈现水平静止状态,X轴与Y轴为0G输出,而Z轴则为1G输出。 地球上所有对象所承受的重力均为1G。若要无人机X轴旋转90度,那么就在X轴与Z轴施以0G输出,Y轴则施以1G输出。倾斜时,XYZ轴均施以0到1G之间的输出。相关数值便可应用于三角公式,让无人机达到特定倾斜角度。
陀螺仪
陀螺仪传感器能监测三轴的角速度,因此可监测出俯仰(pitch)、翻滚(roll)和偏摆(yaw)时角度的变化率。即使是一般飞行器,陀螺仪都是相当重要的传感器。角度信息的变化能用来维持无人机稳定并防止晃动。由陀螺仪所提供的信息将汇入马达控制驱动器,通过动态控制马达速度,并提供马达稳定度。 陀螺仪还能确保无人机根据用户控制装置所设定的角度旋转。
磁罗盘
磁罗盘能为无人机提供方向感。它能提供装置在XYZ各轴向所承受磁场的数据。接着相关数据会汇入微控制器的运算法,以提供磁北极相关的航向角,然后就能用这些信息来侦测地理方位。
气压计
气压计运作的原理,就是利用大气压力换算出高度。 压力传感器能侦测地球的大气压力。 由气压计所提供的数据能协助无人机导航,上升到所需的高度。准确估计上升与下降速度,对无人机飞行控制来说相当重要。
超声波传感器
无人机采用超声波传感器就是利用超声波碰到其他物质会反弹这一特性,进行高度控制。前面就提到过近地面的时候,利用气压传感器是无法应对的。但是利用超声波传感器在近地面就能够实现高度控制。这样一来气压传感器同超声波传感器一结合,就可以实现无人机无论是在高空还是低空都能够平稳飞行。
GPS
GPS是全球导航系统之一,是美国的卫星导航系统。不过最近的无人机开始不单单采用GPS了,有些机型会同时利用GPS与其他的卫星导航系统相结合,同时接收多种信号,检测无人机位置。无论是设定经度纬度进行自动飞行,还是保持定位进行悬停,GPS都是极其重要的一大功能。
麦克风\温湿度
湿度传感器能监测湿度参数,相关数据则可应用在气象站、凝结高度监测、空气密度监测与气体传感器测量结果的修正
麦克风是一种能将声音频号转换为电子讯号的音频传感器
WIFI\蓝牙
无人机有各种不同的联网技术选项可考虑。 低功耗蓝牙(BLE)与Wi-Fi多半用于智能手机联网,Sub-1GHz则是用在远程控制器,能提供更远距离的联网功能。
1.2.3. 如何开发
1.2.3.1. 电脑 PC
1.2.3.2. STLINK JLINK
1.2.3.3. 设备
1.2.3.4. 串口调试
1.2.3.5. KEIL MDK
1.2.3.6. STM32CUBEMX
3. 开发环境搭建
3.1. STM32CubeMX安装
3.1.1. 简介
微控制器图形化配置 自动处理引脚冲突 动态设置确定的时钟树 可以动态确定参数设置的外围和中间件模式和初始化
功耗预测
C代码工程生成器覆盖了STM32微控制器初始化编译软件,如IAR,KEIL,GCC可以独立使用
3.2. HAL库与STD库
1、STD库-标准外设库
寄存器操作,将一些基本的寄存器操作封装成函数
标准库这里会提供一个底层包,用这个包直接调它的API就行。
2、 HAL库-硬件抽象库
Hardware Abstraction Layer (HAL)硬件抽象层
将这些抽象成了一个抽象层,从使用的角度来看,是与硬件无关的
HAL库就是他原本没有这些支持包,现在用HAL给你生成一个支持包,通过图形化的配置,将一些基本的比如GPIO口
3、HAL库优势
HAL库是ST未来主推的库,从2015开始ST新出的芯片已经没有STD库
HAL库的处理机制比STD库好很多,HAL库支持STM32全线产品
HAL库跨芯片的可移植性非常好
3.3. HAL库与STD库区别
1.HAL库就是封装的比较猛,移植性比较强,标准库就是将寄存器封装好,移植性没有HAL好。
2.HAL库可以用ST的软件CUBE生成初始化工程。3.HAL库最方便的就是可以用CUBEMX自动生成代码,动态的调用资源,不会出现地城配置上的冲突。
4.其实就是两种库的区别,Hal库更加全面一点,目前STM32官方也在主推Hal库,目前STMcubemx软件可以直接生成HAL库代码,非常方便编程,易移植。
5.HAL库是ST近年来推出的新库,可以直接在CubeMX下生成例程,并且各个不同型号的STM32之间的函数差异也减少了。
6.HAL库是现在ST主推的库,标准库现在已经不更新了。HAL库做了更深的封装,可以很方便的移植在F0/F1/F3/F4/F7的各个系列的芯片上。
7.hal库通用型强,但是效率稍微低一点,标准库效率高。
8.本质上是一样的,就是配置寄存器,只是HAL库将应用层与驱动层分的比较明确。
9.HAL库设计进一步降低了API对硬件的依赖性,它借鉴了OS中驱动程序的思路,使得API的通用性更强。 能使用ST的CubeMX图形化界面来生成软件框架,它和CubeMX生成的软件代码完全兼容。减少了程序员的负担,同时代码也更规范。 至于与原库函数之间的使用差异, 关键还是要掌握内核及外设的工作原理,如果熟悉了硬件的工作原理,这些库函数还是很好用的。
10.HAL库似乎是为初学者而制定,但这也许是STM32的未来所在。
11.HAL和STD库最大的区别是移植性上的区别,HAL相当于在标准库上在加以封装了。增强了移植性,STD库是在寄存器的基础上封装了一次。12.hal库封装的更想arduino,移植性强,相对效率就低一些,在时间就是金钱的现在,hal库无疑是很好的选择。
13.HAL库和标准库都是对寄存器操作的封装,但是这些库的函数不同在HAL库每个.c文件的开头会介绍这个库里面包含哪些函数,这些函数的用途,可以留意一下。
14.标准库是STM32最早推出的库,应用非常广泛,但是比较新的F7和H7等系列已经不支持了。
HAL库是官方主推的库,目前支持所有系列,相对效率没有标准库高,但是各个系列之间的兼容性很好,而且能够配合STM32CubeMX进行使用。由于官方现在不在更新和支持标准库了,所有精力都放在了HAL库和LL库上了,所以建议今后学习和应用还是以HAL库和LL库为主要对象。
15.hal库和标准库的最大区别就是减少了不同系列器件之间的库函数层差异,并且可以直接用cubemx生成。
16.HAL移植性比较强,可以通过cubemx生成代码,不过效率比较低标准库更像寄存器的操作,感觉更符合对寄存器使用的理解。
17.HAL库移植性比较好,操作比较简单。标准库移植性没那么方便,操作比较复杂,直接对寄存器的操作。
18.HAL库的封装比较多,大部分都是面向对象的设计,移植比较简单。
19.HAL的优点就是用API的设计,十分方便移植,而且操作也简单。
20.HAL的移植性是最好的,但是标准库不太方便移植,所以ST出了一个HAL库。
4. GPIO编程
4.1. GPIO的基本概念及寄存器介绍
GPIO接口简介
通用输入输出接口GPIO是嵌入式系统、单片机开发过程中最常用的接口,用户可以通过编程灵活的对接口进行控制,实现对电路板上LED、数码管、按键等常用设备控制驱动,也可以作为串口的数据收发管脚,或AD的接口等复用功能使用。因此其作用和功能是非常重要的。
GPIO功能复用
STM32有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。
GPIO寄存器介绍:
什么是寄存器:
寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成
GPIO寄存器:
4个32位配置寄存器
GPIOx_MODER
GPIOx_OTYPER
GPIOx_OSPEEDR
GPIOx_PUPDR
2个32位数据寄存器
GPIOx_IDR 、GPIOx_ODR
GPIOx_ODR
1 个32 位置位 / 复位寄存器
GPIOx_BSRR
可以用这个寄存器来写灯
只把相应的位置位复位就可以了,
GPIO_PIN_SET高电平,此时灯关,低电平的时候灯开
只需要把对应的位置0就可以实现低电平,然后控制灯开
这样就把蓝黄绿灯点亮了。
2 个 32 位复用功能配置寄存器
GPIOx_AFRH 、GPIOx_AFRL
1.4.2. 点亮LED
模式配置:
输入模式有4种:浮空输入、上拉输入、下拉输入、模拟输入。
浮空输入:可读取引脚电平,引脚悬空不确定输入的电平。
上拉输入:可读取引脚电平,内部默认接上拉电阻,即默认输入高电平。
下拉输入:可读取引脚电平,内部默认接下拉电阻,即默认输入低电平。
模拟输入:引脚直接连接片上AD模块,把外部模拟信号做AD转换时接入引脚设置为模拟输入模式。
输出模式有4种:推挽输出、开漏输出、复用推挽输出、复用开漏输出。
推挽输出:可认为是有输出高电平能力的输出,IO口具有输出高电平的能力。
开漏输出:需连接上拉电阻才可输出高电平。
复用推挽输出:类似推挽输出,区别是输出由片上外设而非IO口寄存器定义,比如SPI、IIC等外设
复用开漏输出:类似开漏输出,区别是输出由片上外设而非IO口寄存器定义,比如SPI、IIC等外设
Analog:模拟
图详见《 STM32F0数据手册》118页
施密特触发器:作用是判断高低电平,起到一个比较的作用,当你输入一个高于它一个比较电压的时候,比如单片机是输入一个高于0.7的就算高电平,低于0.3的就算低电平。这时就会有一个0.3到0.7之间的差距,有时候写程序有问题的时候,可能是卡在这个区间里它识别不到,大概是这个区域,有误差。
复位就是从1变成0
置位就是设置成1
GPIO寄存器:
输出控制
按键:
(PA8配置成GPIO_input)
(配置成GPIO模式等信息)
(下拉设置,因为按键端按下是高电平,所以我们配置成下拉,这样默认情况下是低,可以更稳定)
(读取按键电平信息)
(HAL_RCC_GPIOA_CLK_ENABLE 使能用的,如果不是A需要换下)
(HAL_GPIO_Togglepin 输出时的高低电平翻转函数)
(翻转函数内部)
效果就是按一下亮,再按一下灭,这样循环。
按键流水灯:使用switch来做
switch复习:
代码:
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
switch (num)
{
case 0:
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8))
{
HAL_Delay(100);
HAL_GPIO_WritePin(GPIOB, GREEN_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, BLUE_Pin, GPIO_PIN_RESET);
while (!HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8))
{
num++;
break;
}
}
break;
case 1:
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8))
{
HAL_Delay(100);
HAL_GPIO_WritePin(GPIOB, BLUE_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, YELLOW_Pin, GPIO_PIN_RESET);
while (!HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8))
{
num++;
break;
}
}
break;
case 2:
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8))
{
HAL_Delay(100);
HAL_GPIO_WritePin(GPIOB, YELLOW_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GREEN_Pin, GPIO_PIN_RESET);
while (!HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8))
{
num++;
break;
}
while (1)
{
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8))
{
HAL_Delay(200);
while ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8)))
{
};
num++;
switch (num)
{
case 1:
HAL_GPIO_WritePin(GPIOB, BLUE_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, GREEN_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, YELLOW_Pin, GPIO_PIN_SET);
break;
case 2:
HAL_GPIO_WritePin(GPIOB, YELLOW_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, BLUE_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GREEN_Pin, GPIO_PIN_SET);
break;
case 3:
HAL_GPIO_WritePin(GPIOB, GREEN_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, YELLOW_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, BLUE_Pin, GPIO_PIN_SET);
break;
default:
num = 0;
break;
}
}
}
5. 通信的基本概念
串口通信:
通信,最少要有两个对象,一个收,一个发
同步通信:
一般情况下同步通信指的是通信双方根据同步信号进行通信的方式。比如通信双方有一个共同的时钟信号,大家根据时钟信号的变化进行通信
异步通信:
是指数据传输速度匹配依赖于通信双方有自己独立的系统时钟,大家约定好通信的速度。异步通信不需要同步信号,但是并不是说通信的过程不同
串行通信 并行通信
串行通信:指的是同一时刻只能收或发一个bit位信息。因此只用1根信号线即可。
并行通信:指的是同一时刻可以收或发多个bit位的信息,因此需要多根信号线才行
-串行传输:数据按位顺序传输。
-优点:占用引脚资源少
-缺点:速度相对较慢
-并行传输:数据各个位同时传输。
-优点:速度快
-缺点:占用引脚资源多
全双工 半双工 单工
单工:要么收,要么发,只能做接收设备或者发送设备。比如(收音机)
半双工:可以收,可以发,但是不能同时收发, 比如(对讲机)
全双工:可以在同一时刻既接收,又发送 (手机)
常见通信总结
USART介绍:
概念介绍:
通用异步收发器(UART)灵活地与外部设备进行全双工数据通信,满足外部设备对工业标准NRZ异步串行数据格式的要求。USART通过小数波特率发生器提供了多种波特率。它支持同步单向通信和半双工单线通信;还支持LIN(局域互联网络),智能卡协议与IrDA(红外数据协会)SIR ENDEC规范,以及调制解调操作(CTS/RTS)。而且,它还支持多处理器通信。
USART支持同步模式,因此USART 需要同步始终信号USART_CK(如STM32 单片机),通常情况同步信号很少使用,因此USART和一般单片机UART使用方式是一样的,都使用异步模式。
这里这个CH340是串口的USB转TTL信号的。
STM32F051串口
串口电路连接
串口通信协议介绍
波特率
在串行通信中,用“波特率”来描述数据的传输速率。所谓波特率,既每秒传送的二进制位数,其单位为bps(bits per second)。它是衡量串行数据速度快慢的重要指标。
国际上规定一个标准的波特率系列: 110、300、600、1200、1800、2400、4800、9600、115200、14.4Kbps、19.2Kbps、……
例如:115200bps、指每秒传送115200位。通信双方必须设置同样的同学速率才能正常通信
注意:实际的数据没这么多,还包括起始位,结束位,校验位
(选择串口功能)
(配置成异步通信Asynchronous)
USART寄存器介绍::
控制寄存器——USART_CR
状态寄存器——USART_SR
数据寄存器——USART_DR
波特率寄存器——USART_BRR(不要纠结,这个计算比较难,直接用函数就可以)
STM32实现串口数据的收发:
串口的发送函数:
HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
函数内部有用到寄存器
用一下这个函数:
打开串口调试助手,按一下复位键就会有发送的消息:
串口的接收:
HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
函数内部有RDR:
串口寄存器
控制寄存器CR1、CR2
波特率寄存器BRR
中断和状态寄存器ISR
数据发送寄存器TDR
数据接收寄存器RDR
printf重定向
printf函数调用的是c库中的fputc函数。因此我们如果重新写了fputc函数,就可以改变printf函数的功能,可以向串口打印输出。
int fputc(int ch, FILE *f) //重定向函数
{
while (!(USART1->ISR & (1 << 7)))
;
USART1->TDR = ch;
return ch;
}
int rdrc() //寄存器方式接收一个字节
{
int ch;
while (!(USART1->ISR & (1 << 5)))
;
ch = USART1->RDR;
return ch;
}
void gettc(int ch) //寄存器方式发送一个字节
{
while (!(USART1->ISR & (1 << 7)))
;
USART1->TDR = ch;
}
写一下程序
烧录了之后按复位,打开串口调试助手,就会看到一直在打印
然后可以改一下直接用这个函数发送一个字符:
烧录复位后打开串口调试助手可以看到一直在发一个字符c
数据只管写到TDR,然后移位寄存器再将数据移出然后发送
就是这个移位寄存器程序里不用管,它硬件上直接就往外移了,就是在搬砖,有砖就搬有砖就搬
再写一个接收的程序:
下面改成接收的这个函数
然后烧录复位,打开串口调试助手可以看到点一下发送就会回复
6. STM32中断系统
中断的基本概念
处理器中的中断
在处理器中,中断是一个过程,即CPU在正常执行程序的过程中,遇到外部/内部的紧急事件需要处理,暂时中止当前程序的执行,转而去为处理紧急的事件,待处理完毕后再返回被打断的程序处继续往下执行。中断在计算机多任务处理,尤其是即时系统中尤为重要。比如uCOS,FreeRTOS等。
意义:
中断能提高CPU的效率,同时能对突发事件做出实时处理。实现程序的并行化,实现嵌入式系统进程之间的切换
中断处理过程
STM32F0中断的体系结构
下面这个图需要了解一下他们大概的位置关系:
中断和异常向量表
STM32F0中断和异常向量
Cortex-M0内核可以处理15个内部异常,和32个外部中断。
STM32F051实际上只使用了6个内部异常和28个外部中断。
当异常或中断发生时,处理器会把PC设置为一个特定地址,这一地址就称为异常向量。每一类异常源都对应一个特定的入口地址,这些地址按照优先级排列以后就组成一张异常向量表。
向量化处理中断的好处
传统的处理方式需要软件去完成。采用向量表处理异常,M0处理器会从存储器的向量表中,自动定位异常的程序入口。从发生异常到异常的处理中间的时间被缩减。
中断和异常向量表
注:中断和异常的区别:
中断是微处理器外部发送的,通过中断通道送入处理器内部,一般是硬件引起的,比如串口接收中断,而异常通常是微处理器内部发生的,大多是软件引起的,比如除法出错异常,特权调用异常等待。不管是中断还是异常,微处理器通常都有相应的中断/异常服务程序
外部中断控制器EXTI
在 STM32F05x 中,共有最多 28 中断 / 事件线可用
GPIO 口连接到 16 个外部中断 / 事件线
系统配置控制器 (SYSCFG)
SYSCFG 外部中断配置寄存器 1-4 (SYSCFG_EXTICR1—4)
外部中断 / 事件框图
按键中断
配置步骤:
这里能找到:
还可以这样:
复制 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
放到gpio.c里,找个位置放
但一定要放到BEGIN和END中间,就是防止再生成程序的时候给生成没了
向量表
GPIO模式配置
还有一个问题
按键中断了还需要削抖吗?
不需要。因为是上升沿或者是下降沿触发,直上直下没有抖动这个问题了。抖动是因为手碰或者一些机械元件接触,机械元件接触的时候它是不稳定的。
串口中断
串口中断分为接收中断和发送中断。
之前这个图:
串口有一个直接可以到NVIC的一个中断,所以串口也是可以用中断来识别的,就是接收过来信号有接收中断,发送过来的有发送中断,这两个。
打开CubeMX对串口的发送中断进行配置:
右键跳转:
如果上面下划找不到,还可以
找到要用的复制到usart.c
然后找到这个:
usart.c:
main.c:
另一个程序:
复制了放到usart.c里边,为什么放这里边,归类一下
烧录打开串口调试助手就可以看到:
再来分析一下:
串口发送完成中断:
实验分析:网络指示灯正常1s闪烁,串口发送指定的字符个数,当发生完成时,切换发送指示灯状态,并打印“发送完成”。
实验步骤:
1)配置CubeMX
USART1 - 异步 - 115200bps
PB1 - GPIO_OUTPUT - HIGH
PB0 - GPIO_OUTPUT - HIGH
NVIC - 使能USART1 global interrupt
2)代码编写
在main.c实现网络指示灯闪烁
while (1)
{
HAL_GPIO_TogglePin(GREE_GPIO_Port, GREE_Pin);
HAL_Delay(1000);
}
在uart.c实现中断服务程序
先找到发送完成的中断回调函数
打开启动代码start.s
107行 DCD USART1_IRQHandler ; USART1
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
}
调用了串口中断服务程序,将串口1的句柄作为参数进行传递。
在HAL_UART_IRQHandler的2577行
/ UART in mode Transmitter (transmission end) -----------------------------/
if (((isrflags & USART_ISR_TC) != 0U) && ((cr1its & USART_CR1_TCIE) != 0U))
{
UART_EndTransmit_IT(huart);
return;
}
串口发送结束中断
static void UART_EndTransmit_IT(UART_HandleTypeDef *huart)
{
ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_TCIE);
huart->gState = HAL_UART_STATE_READY;
huart->TxISR = NULL;
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
huart->TxCpltCallback(huart);
#else
HAL_UART_TxCpltCallback(huart);
#endif
}
找到串口发送完成中断回调函数:
HAL_UART_TxCpltCallback(huart);
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
UNUSED(huart);
}
在uart.c重写HAL_UART_TxCpltCallback
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
HAL_UART_Transmit(&huart1,"Transmit complete!",sizeof("Transmit complete!"),
HAL_GPIO_TogglePin(BLUE_GPIO_Port,BLUE_Pin);
}
}
回到main.c
HAL_StatusTypeDef HAL_UART_Transmit_IT (UART_HandleTypeDef huart, uint8_t pData, uint16_t Size)
参数:UART_HandleTypeDef * huart 句柄
uint8_t * pData 发送数据的首地址
uint16_t Size 发送数据的长度
HAL_UART_Transmit_IT(&huart1,"hello\n",strlen("hello\n"));
//发送“hello”,发送完成后调用发送完成中断回调函数,打印"Transmit complete!"。
通过串口一次发送多个字符
代码:
/* USER CODE BEGIN 0 */
void uart_putchar(uint8_t ch)
{
while(!(USART1->ISR & (1<<7))); //while等待发送寄存器为空,ISR第七位为1说明发送寄存器为空,则应该跳出循环向下执行。
USART1->TDR = ch;
}
//通过串口接收字符函数
uint8_t uart_getchar()
{
uint8_t ch;
while(!(USART1->ISR & (1<<5))); //等待接收数据寄存器非空,ISR的第五位为1说明接收寄存器里接收到了数据。
ch = USART1->RDR;
return ch;
}
//通过串口发送字符串函数
void uart_puts(uint8_t *s)
{
while(*s)
{
uart_putchar(*s);
s++;
}
}
//通过串口接收字符串函数
void uart_gets(uint8_t *s)
{
while(1)
{
*s = uart_getchar();
if(*s == ' '||*s == '\n') // 字符串通常以空格和反斜杠n结尾
{
break;
}
s++;
}
}
/* USER CODE END 0 */
main.c
外层:
烧录复位后打开串口调试助手:
7. STM32时钟系统
1.时钟的概念
1)时钟是嵌入式系统中的脉搏,处理器内核是时钟的驱动下完成指令执行、状态切换等动作的。
外部设备在时钟驱动下完成各项工作。比如:串口通信、AD转换、定时器计数...
因此时钟对我们计算机系统来说至关重要,通常来说时钟出现问题,一般都是致命性问题。
2)时钟系统由时钟源(振荡器)、定时唤醒器、分频器、倍频器等组成的电路。
常用的信号源有:
RC振荡器
优点:成本低,由电阻电容构成。启动时间短。
缺点:精确度存在问题,不够稳定,容易受到环境因素影响。
晶体振荡器
优点:时钟信号更加稳定、精度高
缺点:价格高、需要较长的起振时间。
倍频器:CPU需要较高的频率,制作高频振荡器成本较高,就可以利用倍频器对现有的时钟频率进行倍频来供给CPU。
分频器:外部设备需要不同的时钟频率进行工作,可以通过分频器产生不同的时钟频率供给各个外设。
面试题:为什么要有倍频器和分频器?
答: 1)为了降低成本(CPU需要更高的时钟频率)
2)减少功耗 (外设需要不同的时钟频率)
2.STM32有四个时钟源
HSI:高速内部时钟,RC振荡器,频率为8MHz;
HSE:高速外部时钟,可接石英/陶瓷谐振器,4到32 MHz外部振荡器可为系统提供非常精确的主时钟。
LSI:低速内部时钟,RC振荡器,频率为40KHz。它可以在停机和待机模式下保持运行,为独立看门狗和 RTC 提供时钟
LSE:低速外部时钟,接32.768KHz的石英晶体。它为实时时钟或者其他定时功能 提供一个低功耗且精确的时钟源
PLL:锁相环倍频输出,它的时钟输入源也是HSE或HSI经过分频得到的。
有时候会说5个,但是从源头上来说还是4个,因为PLL是经过分频得到的。
STM32Cubemx配置时钟:
HAL_Delay()分析(1)
SysTick定时器工作原理图
3.时钟复位控制 RCC(reset clock controller)
RCC 通过 AHB 时钟 (HCLK)8 分频后作为 Cortex 系统定时器 (SysTick) 的外部时钟。
通过对 SysTick 控制与状态寄存器的设置,可选择上述时钟或 Cortex(HCLK) 时钟作为 SysTick 时钟。
中文参考手册84页 图10 时钟树 (重点看)
SysTick 滴答定时器 :
概念:
能够用于定时、计数的器件称为定时器。
systick是在Cortex-M0内核内部的,跟NVIC捆绑在一起的,产生systick异常用于对输入的时钟进行计数。
工作原理:
滴答定时器本质上就是一个24位递减计数器,也就是最大计数是2^24 - 1(0xFFFFFF)
在给Systick设定初值后,每当到来一个时钟信号,计数值则减1,直到计数值减到0时,
触发一次异常事件,处理异常服务程序,处理完成后计数器再自动重装初值并继续减一,依次循环。
HAL_Delay()分析2:
SysTick寄存器:
比如说重装载值是100,当前值是0(当前值就是这个减数过程中值的数字,我们如果想要让它更精确,就把它在延时的时候先把它放在初始值这里,让它从0开始,装回去,从100再减,这样就保障了延时的准确性),怎么达到100us呢,就是100一直减减到0,然后又重新回来100。
知道频率怎么算周期呢?
周期就是频率的倒数
48MHz的,用1us要记48个数
所以把重装载值寄存器LOAD记48:
1微秒是48,多少微秒就乘以多少就得到了我们想要得到的延时数。
然后去中文参考手册去找时钟,
AHB外部时钟使能寄存器(RCC_AHBENR)
然后发现第18位是GPIOB的:
我们用一个东西,首先得开启它的时钟
复制后进到gpio.c中放在GPIO初始化函数void MX_GPIO_Init(void)中,然后把下划线改成->
然后把第18位置1,使能一下:
然后对GPIO口进行配置,首先有一个输入输出的配置,咱们这是输出,输出有一个速度,还有一个是开漏还是推挽,主要是这些,上拉下拉可以配置也可以不配置。
然后就去找GPIO的寄存器
GPIO 端口模式寄存器 (GPIOx_MODER) (x = A..D,F)
寄存器分析:
同样复制寄存器的名字GPIOx_MODER,然后把下划线改成->
然后看速度:
GPIO 口输出速度寄存器 (GPIOx_OSPEEDR) (x = A..D,F)
然后把速度这个置0一下:
GPIO_PIN_0的在第0位置0;GPIO_PIN_1的在第2位置0;GPIO_PIN_2在第4位置0。
然后配置输出类型:
GPIO 端口输出类型寄存器 (GPIOx_OTYPER) (x = A..D,F)
GPIO_PIN_0的在第0位置0,设置推挽输出:
GPIO_PIN_1在第1位置0:
GPIO_PIN_2在第2位置0:
然后我们想给它一个默认的电平,默认让它灭
其他两个灯也同理
下面看一下比如直接写GPIO_PIN_0是怎么样的原理:
写好了之后去到main函数里
低电平亮的话,指向BRR也行
端口位复位寄存器 (GPIOx_BRR) (x=A..G)
STM32TIM定时器:
功能
三种定时器区别:
计数模式:
通用定时器可以向上计数、向下计数、向上向下双向计数模式。
①向上计数模式:计数器从0计数到自动加载值(TIMx_ARR),然后重新从0开始计数并且产生一个计数器溢出事件。
②向下计数模式:计数器从自动装入的值(TIMx_ARR)开始向下计数到0,然后从自动装入的值重新开始,并产生一个计数器向下溢出事件。
③中央对齐模式(向上/向下计数):计数器从0开始计数到自动装入的值-1,产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器溢出事件;然后再从0开始重新计数。
合并图:
中心对齐就是到中间就已经重新装了,相当于走一个向上计数的也走一个向下计数的。
寄存器
这儿有个计数器,相当于当前值什么的,对数据进行计数的;
配置
选第二个:
时钟源->内部时钟
这里配置的是48MHz
这里48减1是1us,如果配置72MHz的就写72-1,也是1us,效果一样的
如果想用1s的话就填0,1加上六个0
然后继续配置:
生成工程后在.s文件里可以跳转:
F051的在3949行
复制之后去tim.c里找个地方
记得在tim.c里加头文件:
在主函数中要开启一下:
分析一下:
有了这个公式,就能计算延时时间:
例如:
还有:
寄存器
软件定时器应用:
利用软件定时器实现LED1s闪烁:
直接去文件夹下新建一个.h
代码:
main函数里:
烧录后实际的效果就是蓝黄绿间隔1s闪烁。
记录
系统定时器,先获取一个当前值,就是程序从上电或者复位开始,存上这个当前值后,就开始一直获取这个最新的,和那个存储的当前值相减,
自己写的软件定时器:
先把当前时间获取到,用HAL_GetTick()获取,需要再有一个定时的时间,定时的时间是自己设置的,以毫秒为单位,比如想定500ms就传一个参数500就好,用结构体封装这两个变量:当前值(获取到就固定了)和传的定时时间,然后把刚才的参数转换一下子,转换就是再写一个类似系统写的延时器,比如先命名一个结构体MyTim,然后不断的获取最新的时间,最新的时间获取了之后减去上边那个一开始的当前时间,这样就得出一个时间差就是你运行了多久,只要大于等于那个延时时间,就说明这个时间到了,就return个1,没到就return个0,习惯性正确的是1不正确的是0,然后就判断这个返回的数值是1还是0。
这个就是在while里轮询着,面试时候问你这个是什么算法,就说轮询
pwm需要用到的东西
电阻是干什么的?限流、分压
电容 :滤波,按键有抖动,加电容就把抖动时间弄过去了
数码管什么原理,ABCDEFG加一个点.八段,每一个都是二极管,
共阴就是公共接的都是阴,共阳就是接的阳
蜂鸣器:分两种,有源和无源
无源就是里面有个小铜铝片,两边连出来,直接输高低电平是不会响的,需要一边一直哆嗦。
有源的就类似咱们时钟
【1】定时器
STM32F0系列器件包含6个通用定时器、1个基本定时器、1个高级定时器
a.通用定时器TIM2 3 14 15 16 17功能
● 定时器定时,定时器计数
● 输入捕获
● 输出比较 - 输出PWM
b.高级定时器TIM1功能
● 通用定时器的有功能
● 带死区控制和紧急刹车,可用于PWM控制电机【*】
c.基本定时器TIM6功能
● 主要运用于定时计数以及驱动DAC
定时器计数模式
向上计数模式:计数器从0开始计数,当达到自动装载寄存器(TIMx_ARR)里的值时,自动清零且产生一个溢出事件,然后再从0开始向上计数。。
向下计数模式:计数器从自动装载寄存器(TIMx_ARR)里的值开始递减计数,当计数值达到0时产生一个定时器溢出事件,并重装初值,继续向下计数。
中央对齐模式:又称为向上/向下计数,计数器从0开始递增达到ARR的值,产生一个定时器溢出事件,再从ARR的值递减到0,产生一个定时器溢出事件。
定时器计数原理:
时钟频率配置成了48Mhz,如何让定时器产生1s中断?
分频值写0相当于不分频 48/1 => 48
先对主频进行48分频得到1Mhz的频率,则分频值为48-1
48Mhz / 48 => 1Mhz
1Mhz的时钟频率,相当于计一个数需要1/1000000秒,
所以如果想得到1s中断,则需要从0开始计数到1000000-1,即计1000000个数需要1秒。
1000000个数 * 1/1000000s = 1s
频率是时间的倒数 1Mhz = 1/1000000s
实验一:定时器定时打印
实验分析:利用定时器实现1秒中断,在定时器的中断服务程序中打印“helloworld”
实验步骤:
配置CubeMX
1)设定时钟频率为48M
RCC - 晶体/陶瓷谐振器 - 主频48Mhz
2)可以使能高速外部时钟 - RCC - Crystal/Ceramic Resonator晶体/陶瓷谐振器 - OSC-IN OSC-OUT
使能串口USART1 - 异步通信ASY - 115200bps
3)配置通用定时器TIM2
TIM2 - 时钟源 - 内部时钟 - APB - 48Mhz
分频值 - 48分频 - 48 - 1
计数模式 - 向上计数
自动重载计数器 - 1000000 - 1
编写代码:
找到中断服务程序的入口地址
DCD TIM2_IRQHandler ; TIM2
TIM2的中断服务程序
void TIM2_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim2);
//定时器中断服务程序
}
进入HAL_TIM_IRQHandler:
在3940行 定时器计数事件
/* TIM Update event */
if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE) != RESET)
{
if (__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_UPDATE) != RESET)
{
__HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE);
#if (USE_HAL_TIM_REGISTER_CALLBACKS == 1)
htim->PeriodElapsedCallback(htim);
#else
HAL_TIM_PeriodElapsedCallback(htim); //定时器溢出中断回调函数
#endif /* USE_HAL_TIM_REGISTER_CALLBACKS */
}
}
在tim.c里重写定时器溢出中断回调函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2)
{
HAL_UART_Transmit(&huart1,"helloworld",strlen("helloworld"),1000);
}
}
在main.c中启动定时器2的计数中断
HAL_TIM_Base_Start_IT(&htim2);
实验二:软件定时器
HAL_Delay() 硬延时 cpu在等着达到延时时间后才会继续做其他工作
设计思想:
1.设计定时器(记录起始时间,记录用户延时时间)
typedef struct
{
uint32_t start; //保存起始时间
uint32_t delay; //保存延时时间
}MyTim;
2.提供一个用于传递定时时间的软件定时器配置函数
//定时器初始化函数
void setTim(MyTim *timer,uint32_t delayms)
{
timer->start = HAL_GetTick();
timer->delay = delayms;
}
3.提供一个用于比对是否达到定时要求的函数
uint32_t compareTim(MyTim *timer)
{
if(HAL_GetTick() - timer->start >= timer->delay)
{
return 1; //返回1说明达到了定时要求
}else
{
return 0;
}
}
HAL_Delay()分析2
PWM模式解析可以在中文参考手册这找到:
今天两个知识点:
输入捕获:用于捕获外部事件,比如引脚的电平变化记录变化时间,
用于测量外部信号的频率或某个电平持续时间。
输出比较:用于控制输出一个波形,用计数器跟比较寄存器进行比对,
做出相应动作,比如翻转电平,通常用于产生PWM方波信号。
实验三:通过定时器产生PWM信号,驱动蜂鸣器
蜂鸣器是什么?
采用直流电压供电的一个电子讯响器。
蜂鸣器分类:
有源蜂鸣器:内部带有震荡源,一通电就可以震荡发出响声,驱动较容易。
因为是内部集成好的震荡电路,所以频率是固定的。
无源蜂鸣器:内部没有震荡源,直流电无法驱动,所以用一个方波信号来进行驱动,
价格便宜,且频率可控。需要通过编程控制声调和响度,驱动稍麻烦。
方法一:通过给D1管脚输出高低电平来驱动蜂鸣器发声
将D1对应的管脚PB8配置成GPIO_OUTPUT
在main.c中
while (1)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_7);
HAL_Delay(1);
}
方法二:定时器控制蜂鸣器发声
计数器寄存器 (TIMx_CNT)
自动装载寄存器 (TIMx_ARR)
捕获/比较寄存器(TIMx_CCRx)
输出过程:
当0-t1这段时间,计数器寄存器的CNT的值是小于CCR,输出低电平。
当t1-t2这段时间,计数器寄存器的CNT的值是大于CCR且小于ARR的,输出高电平。
当CNT的值达到ARR里的值时,产生溢出事件,自动清零再次从0开始向上计数。
实验步骤:
查看原理图:
蜂鸣器模块原理 - D1
底板原理图 - D1 - 核心板底座
核心板原理图 - 核心板插针 - PB7
CubeMX配置:
//将PB7配置成PWM信号的输出端
1.将时钟主频配置48Mhz
2.因为PB7管脚只对应TIM17的输出通道,因此需要启动TIM17定时器
TIM17 - act - 选择PWM输出通道CH1N
分频系数 48-1 (48Mhz的时钟经过48分频得到1Mhz的时钟频率)
Counter 5000-1 (相当于给到ARR寄存器 自动重装初值)
Pulse 2500 (脉冲计数个数即为高电平持续的时间 - 占空比)
时钟频率是1Mhz,计一个数就是1/1000000s即1微秒,计5000个数则需要5ms,
因此得知生成的PWM信号的一个周期就是5ms, 频率即为时间的倒数,
所以频率就是1 / 0.005s = 200Hz。
占空比:高电平占用整个周期的时间
0-2500是输出高电平信号的,2500到5000是输出低电平,高电平持续时间占整个周期的50%,
所以该PWM信号的占空比即为50%
编写代码:
//启动TIM17定时器输出PWM信号
HAL_TIMEx_PWMN_Start(&htim17,TIM_CHANNEL_1);
周期 - 频率 : 周期越短,频率越高,则音调越高(尖)
周期越长,频率越低,则音调越低(闷)
脉冲 - 占空比 : 高电平持续时间越长,响度越大
高电平持续时间越短,响度越小
day6
脉宽调制和正弦信号发生器
有这样一个波形:
这叫脉宽调制,有人万一说脉宽调制这个词,至少大概能想到有个在PWM这
简单处理一下可以变成这种正弦的波形:
想做一个STM正弦的信号发生器,有一个最简单的方法,就是使用SPWM,就是直接在TIM定时器引脚这里输出一个SPWM,这样的情况下,外面串一个电阻并一个电容,就出来正弦波了,这是最简单的一个信号发生器,也是最快速的一个。
有面试要问的话,知道这个肯定更好。
SPWM_百度百科 (baidu.com)
PWM的全称是Pulse Width Modulation(脉冲宽度调制),它是通过改变输出方波的占空比来改变等效的输出电压。广泛地用于电动机调速和阀门控制,比如电动车电机调速就是使用这种方式。
所谓SPWM,全称是Sinusoidal Pulse Width Modulation(正弦脉宽调制),就是在PWM的基础上改变了调制脉冲方式,脉冲宽度时间占空比按正弦规律排列,这样输出波形经过适当的滤波可以做到正弦波输出。它广泛地用于直流交流逆变器等,比如高级一些的UPS就是一个例子。三相SPWM是使用SPWM模拟市电的三相输出,在变频器领域被广泛的采用。
定长接收、不定长接收
通过包头包尾来确定,确定一开头,就证明需要用的数据开始传了,然后再往后走,
CubeMX生成PWM,驱动蜂鸣器
或者
实验步骤:
查看原理图:
蜂鸣器模块原理 - D1
底板原理图 - D1 - 核心板底座
核心板原理图 - 核心板插针 - PB7
CubeMX配置:
//将PB7配置成PWM信号的输出端
1.将时钟主频配置48Mhz
2.因为PB7管脚只对应TIM17的输出通道,因此需要启动TIM17定时器
TIM17 - act - 选择PWM输出通道CH1N
分频系数 48-1 (48Mhz的时钟经过48分频得到1Mhz的时钟频率)
Counter 5000-1 (相当于给到ARR寄存器 自动重装初值)
Pulse 2500 (脉冲计数个数即为高电平持续的时间 - 占空比)
时钟频率是1Mhz,计一个数就是1/1000000s即1微秒,计5000个数则需要5ms,
因此得知生成的PWM信号的一个周期就是5ms, 频率即为时间的倒数,
所以频率就是1 / 0.005s = 200Hz。
占空比:高电平占用整个周期的时间
0-2500是输出高电平信号的,2500到5000是输出低电平,高电平持续时间占整个周期的50%,
所以该PWM信号的占空比即为50%
编写代码:
//启动TIM17定时器输出PWM信号
HAL_TIMEx_PWMN_Start(&htim17,TIM_CHANNEL_1);
周期 - 频率 : 周期越短,频率越高,则音调越高(尖)
周期越长,频率越低,则音调越低(闷)
脉冲 - 占空比 : 高电平持续时间越长,响度越大
高电平持续时间越短,响度越小
滴滴滴滴滴:
每个定时器有4个通道,每个通道都有一个捕获比较寄存器(即获取CNT的值,与CCRx进行比较),通过比较输出高低电平,以此来实现对脉宽的调制(PWM)
TIMx_ARR寄存器确定PWM频率:
通过输入的重装载值,值越大,计的数越多,所以就越慢
TIMx_CCRx寄存器确定占空比:
比较寄存器的值越大,占空比越小
刚刚设置的是100,然后上面设置的是5000,如果想把占空比调大,100得变小
PWM模式1(向上计数): 计数器从0加到ARR(自动重装载值),计数器溢出,然后计数器归为0,继续加循环
PWM模式1(向下计数): 计数器从ARR(自动重装载值) 减到0,计数器溢出。然后计数器归为ARR,继续减循环
可以在中文手册里查引脚:
补充:占空比
DMA:
【STM32】 DMA原理,步骤超细详解,一文看懂DMA_Z小旋的博客-CSDN博客_stm32 dma
DMA简介
DMA,全称Direct Memory Access,即直接存储器访问。
DMA 传输方式无需 CPU 直接控制传输(也就是CPU告诉一下就可以了,然后就开始搬运就行了),也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。
●5 个独立的可配置通道 ( 请求 )
● 每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触发。这些配置通
过软件来完成。
● 在同一个 DMA 模块上,多个请求间的优先权可以通过软件编程设置 ( 共有四级:很高、高、
中等和低 ) ,优先权设置相等时由硬件决定 ( 请求 1 优先于请求 2 ,依此类推 ) 。
● 独立数据源和目标数据区的传输宽度 ( 字节、半字、全字 ) ,模拟打包和拆包的过程。源
和目标地址必须按数据传输宽度对齐
● 支持循环的缓冲器管理
● 每个通道都有 3 个事件标志 (DMA 半传输、 DMA 传输完成和 DMA 传输出错 ) ,这 3 个事
件标志逻辑或成为一个单独的中断请求。
● 存储器和存储器间的传输
● 外设到存储器和存储器到外设,外设到外设间的传输
● 闪存、 SRAM 、 APB 和 AHB 外设均可作为访问的源和目标
● 可编程的数据传输数目:最大为 65536
STM32F0-DMA通道
注:可修改SYSCFG_CFGR1来配置通道的映射关系
STM32F0-DMA通道的优先级
仲裁器根据优先级管理着通道的请求和启动外设 / 存储器的访问
优先级管理分两个方面:
● 软件:可通过 DMA_CCRx 寄存器配置每个通道的优先级,优先级分4个等级:
- 最高优先级
- 高优先级
- 中等优先级
- 低优先级
● 硬件:如果 2 个请求有相同的软件优先级,则较低编号的通道比较高编号的通道有较高
的优先权。举个例子,通道 2 优先于通道 4 。
STM32F0-DMA传输
DMA传输模式
1个DMA控制器,5个可独立配置的通道。所有通道都支持memory to memory的传输、外设到外设的传输、以及外设和memory之间的传输。
DMA传输的源、目的、长度
DMA_CPARx 寄存器: 设置外设寄存器地址
DMA_CMARx 寄存器:设置存储器地址
DMA_CCRx 寄存器 : 配置数据的传输方向
如果是存储器到存储器模式,需配置DMA_CCRx的MEM2MEM位
DMA_CNDTRx 寄存器: 写入需要传输的数据量, (0 到 65535)
DMA_CCRx 寄存器中的 PSIZE 和 MSIZE 位:
设置源和目的的数据宽度,两边的位宽尽量保持一致
DMA增量设置
通过设置 DMA_CCRx 寄存器中的 PINC 和 MINC 标志位,外设和存储器的指针在每次传输后可以有选择地完成自动增量
当设置为增量模式时,下一个要传输的地址将是前一个地址加上增量值,增量值取决与所选的数据宽度为 1 、 2 或 4 。
DMA循环模式
循环模式用于处理循环缓冲区和连续的数据传输 ( 如 ADC 的扫描模式 ) 。在 DMA_CCRx 寄存器中的 CIRC 位用于开启这一功能。
当启动了循环模式,一组的数据传输完成时,计数寄存器将会自动地被恢复成配置该通道时设置的初值, DMA 操作将会继续进行。
STM32F0-DMA中断
注:这些标志位都在中断状态寄存器DMA_ISR中设置:
使用DMA方式,实现串口收发
配置
连续添加两个:
这是5个优先级:
在这个函数里写:
extern uint8_t rx_buffer[100]; // 接收数据的数组
extern volatile uint8_t rx_len; // 接收数据的长度
extern volatile uint8_t recv_end_flag; // 接收结束标志位
然后回到main.c
编译烧录打开串口调试助手:
附:
右键跳转后发现:
STM32模数转换器ADC
STM32-ADC模数转换概述
ADC简介
ADC是一个逐次逼近型的模数转换器,可以将连续的模拟信号,转换成离散的数字信号。
模拟信号:电压、温度、光照、压力....(传感器可以将非电学量转换成电学量)
最直观的体现,模拟信号是连续变化的曲线,而数字量是不连续的一个个离散的点。
ADC的作用
采集传感器的数据,测量输入电压,检查电池电量剩余,监测温湿度等。
ADC的性能指标
量程:能测量的电压范围
分辨率:ADC的分辨率通常以输出二进制数的位数表示,位数越多,分辨率越高,一般来说分辨率越高,转化时间越长。
转化时间:模拟输入电压在允许的最大变化范围内,从转换开始到获得稳定的数字量输出所需要的时间称为转换时间
就是从左到右这一整个周期所用到的时间。这里就涉及到时钟了。
STM32的ADC简介
12 位 ADC 是一种逐次逼近型模拟数字转换器。
它有多达 19 个通道,可测量 16 个外部通道(从外部GPIO口连接的16通道模拟输入)
3个内部信号源,分别为内部温度传感 (VSENSE) 输入、内部参考电压 (VREFINT) 输入、外部电池 VBAT 供电引脚输入
各通道的 A/D 转换可以单次、连续、扫描或间断模式执行。
ADC的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中。
STM32F0-ADC特性
12位精度下转换速度可高达1MHz
可配置的转换精度:6位,8位,10位,12位(就是上边画的采集点位的问题)
转换电压范围:0 ~ 3.6V,V SSA ~ V DDA
供电范围:2.4V ~ 3.6V
19个转换通道: 16个外部通道、 3个内部通道
采样时间可配置
ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中
STM32F0-ADC时钟
APB时钟的2或4分频,最高14MHz
优点:不会有时钟域之间的同步带来的抖动,触发事件和转换的起始时刻之间的延迟是确定 的,从 而保证转换之间的时间间隔是固定的
缺点: ADC的转换时间和系统时钟频率相关,受系统频率的影响较大
片上14MHZ HSI RC振荡器
优点:无论MCU的运行频率,都可以保证最高的ADC工作频率可以使用自动节电模式(自动开启或关闭14MHz的内部振荡器)
缺点:触发信号的同步会带来抖动,触发事件和转换的起始时刻之间的延迟不确定
STM32F0-ADC通道的选择
多通道就是两个以上的口都想进来,那怎么办,就轮询,先采集一个再采集一个
19路复用通道
● 16 个从 GPIO 引脚引入的模拟输入 (ADC_IN0...ADC_IN15)
● 3 个内部模拟输入 ( 温度传感、内部参考电压、 VBAT 通道 )
ADC 可以转换一个单一通道或自动扫描一个序列通道。被转换的通道序列必须在通道选择寄存器 ADC_CHSELR 中编程选择:每个模拟输入通道有专门的一位选择位 (CHSEL0...CHSEL18).
STM32F0-ADC转化模式
可以看到优缺点了:
连续转换的占用资源,单次转换的转换的没多通道的及时;但是工程上常用的是单次转换,首先占用的资源少,其次可以控制,想让它转换时就转换,不想转换的时候就不给它使能就不转换了。
注:
ADC 通知应用每次转换结束 (EOC) 事件
ADC 通知应用每次序列转换结束 (EOS) 事件。
这些标志位都是在ADC 中断和状态寄存(ADC_ISR)中
ADC_CFGR1可配置COUNT位 。
STM32F0-ADC转化时间
可编程采样时间 (SMP)
T Sampling 可配置: SMP[2:0]@ADC_SMPR
需要和外部电路的输入阻抗匹配,采样时间适用于所有通道
转化的时间
T conversion 取决于转换精度: RES[1:0]@ADC_CFGR1
每个通道总的转换时间等于:
T Sampling + T conversion
上表可以看出精度越高需要的转换时间越多,为什么呢?
例如采样精度为12位,ADC的时钟为14M:
最小转化时间为:
t CONV = 1.5T + 12.5T = 14 T= 1 µ
转换时间快速预览表
不需要高转换精度的应用,可以通过降低精确度来提高转换速度
假设ADC模块工作在14MHz的最高工作频率下
STM32F0-ADC触发方式
软件触发
软件设置ADC_CR的ADSTART=1 时,触发选择有效。
外部事件触发
外部事件 ( 例如:定时器TRGO、输入引脚 ) 触发,可以设置触发源以及触发极性
STM32F0-ADC模拟看门狗
大概是这么个意思:
就是设置一个电压门槛,类似PWM比较器,比如设置3V,那么外部电压采集到3V了就会立刻跳到一个中断里。比如到3V了就置一个1,然后识别到1了就赶紧干一个活去。
还有软件上的看门狗的:
程序在一直运行过程中,你不知道这个程序有没有卡死,这个看门狗的作用就是看着点,看着有没有死机,
就比如说约定好了每过10分钟去喂一次狗,但是有一次到了点没有去喂,就说明在忙着还没有出来,赶紧想办法把忙着的弄出来
可以去给一个参数+1,每运行一次给一次,这样的话时间久了如果程序卡死了,这个参数得不到变化了,就知道卡死了,或者可以给一个灯来指示报警,或者重启,其实最简单的方法就是直接给一个重启的指令,然后它重启了,这样就可以了
检测待转换的模拟电压
电压超出检测范围就置位AWD@ADC_ISR,并条件性地产生中断
检测范围由上下门限寄存器指定、 12位的ADC_HTR和ADC_LTR有效值
模拟看门狗的使能控制
AWDEN@ADC_CFGR1
检测所有通道还是单个通道由AWDSEL@ADC_CFGR1决定
检测哪个单个通道由AWDCH[4:0]@ADC_CFGR1决定
附:VSCode打开文件后中文注释乱码了
有一个通过内容猜测:
然后就不乱码了:
光敏电阻电路原理图:
STM32CubeMX配置:
IN4是输入的第4个通道
然后去找ADC功能(不用改):
时钟
依旧是把外部时钟设置成48MHz的
实验一:单通道单次采集实验 - 火焰传感器采集
分析原理图:
火焰传感器模块 - OUT - D2 - 高低电平
- IN- - A1 - 模拟量 (ADC采集该管脚即可)
底板原理图 - A1 - 核心板底座
核心板原理图 - 核心板插针 - A1 - PA4
CubeMX配置
UART1 - 异步模式 - 115200bit/s (用串口看的话就这么配置)
火焰传感器 - A1 - PA4 - ADC_IN4
代码编写:
uint32_t fire_value;
while(1)
{
HAL_ADC_Start(&hadc); //启动ADC
HAL_ADC_PollForConversion(&hadc,100); //等待转换
fire_value = HAL_ADC_GetValue(&hadc); //转换完成后将值赋给fire_value
printf("fire = %d\n",fire_value);
HAL_ADC_Stop(&hadc);
HAL_Delay(1000);
}
再写一个重定向函数,让其在串口中显示数据:
int fputc(int ch, FILE *f) //重定向函数
{
while (!(USART1->ISR & (1 << 7)))
;
USART1->TDR = ch;
return ch;
}
插上火焰传感器烧录打开串口调试助手:
实验二:多通道单次采集实验 - 采集按键值和火焰值
1.CubeMX配置
UART1 - 异步模式 - 115200bit/s
火焰传感器 - A1 - PA4 - ADC_IN4
光照传感器 - A2 - PA6 - ADC_IN6
系统时钟 - 48M //时钟影响cpu存取速度
2.代码编写:
uint32_t key,fire;
while (1)
{
HAL_ADC_Start(&hadc);
while(!(ADC1->ISR &(1<<2))); //单个通道转换结束 EOC信号
key = HAL_ADC_GetValue(&hadc);
while(!(ADC1->ISR &(1<<3))); //序列转换结束 EOS信号
fire = HAL_ADC_GetValue(&hadc);
printf("key = %d fire = %d\n",key,fire);
HAL_ADC_Stop(&hadc);
HAL_Delay(1000);
}
多通道是采集不同引脚的值
按键是单通道的,因为只用一个引脚
代码截图:
串口调试:
任务二 蜂鸣器放音乐
五向按键,不同方向控制不同灯效果
这儿为什么uint32_t呢,下边的HAL_ADC_GetValue跳转可以看到返回的类型:
多通道的话,G030的板子是下边这样改,F051的没改
火焰传感器值获取的:
while(1)
{
HAL_ADC_Start(&hadc); //启动ADC
HAL_ADC_PollForConversion(&hadc,100); //等待转换
fire_value = HAL_ADC_GetValue(&hadc); //转换完成后将值赋给fire_value
printf("fire = %d\n",fire_value);
HAL_ADC_Stop(&hadc);
HAL_Delay(1000);
}
注意:G030的需要把&hadc换成&hadc1
PWM呼吸灯
这是G030的,051的CUbemx配置好之后,把主函数复制黏贴就能用
呼吸灯的占空比是不断变化的,找一个函数至少的功能是可以调整占空比数值,
while里循环改变占空比,设一个变量,+++加到一个数,然后清零,然后-----
配置:
main.c中:
uint16_t pwmValue=0;
uint8_t dir=0;
int main(void):
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_3);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(dir==0)
{
pwmValue+=5; // 改变占空比,下边有函数的
if(pwmValue==500)
dir=1;
}
else
{
pwmValue-=5;
if(pwmValue==0)
dir=0;
}
__HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_3,pwmValue); // pwmValue在这里传了参数的
HAL_Delay(10);
}
/* USER CODE END 3 */
SPI
spi常用的除了这个LCD屏幕之外,还有FLASH、ADC等
是全双工通信
LCD显示屏
1. 选择一张图片 - 后缀名修改为xx.bmp
2. 用电脑工具画图 - 打开图片 - 重新调整大小 - 像素 - 128*128 - 保持纵横比去掉 - 保存
3. 图片取模软件 - Image2Lcd 2.9(破解版) - Img2Lcd.exe - C语言数组 水平扫描 16位真彩色 128*128像素 不勾选包含图像头
5. Lcd_Init(); //初始化
Lcd_Clear(RED); //清屏
HAL_Delay(1000); //延时一短时间
showimage(gImage_xx); //传入图片的数组名
将lcd源码的.c和.h文件放在工程目录对应的位置下。
液晶:
1)某些物质在熔融状态下具备液体的流动性,保留了部分晶体物质分子的有序排列,
形成一种兼有晶态和液态部分形式的中间态。
2) 在通电状态下,可以有序排列,使光线容易通过;
在不通电状态下,排列混乱,阻止光线通过。
液晶显示屏的显色原理:
液晶显示屏必须要有背光源提供背光,荧光灯投射出光源,先经过偏光片到达液晶层,通过通断电控制液晶的排列方式,进而改变穿过液晶层的光线角度,再经过滤光片和偏光片之后,就能投射出不同颜色的光。因此改变刺激液晶排列方式的电压进而实现对最终呈现颜色和亮度的控制。
24位真彩色
R:8位
G:8位
B:8位
颜色深度:2^8 2^8 2^8
STM32F051开发板的板载显示屏是1.44寸的TFT液晶显示屏,
128*128 16位色 RGB565
R:5
G:6
B:5
颜色深度:2^5 2^6 2^5
R G B
红色 11111 000000 00000
SPI通信总线
1.概念:
SPI接口是同步全双工三线/四线串行总线,采用主从模式架构(一个主机多个从机)
四线制:
MOSI : 数据线 主设备数据输出,从设备数据输入
MISO : 数据线 主设备数据输入,从设备数据输出
SCLK :时钟线 主机产生时钟信号
SS :片选线 由主机控制选择从机设备(低电平有效)
单主机单从机,一对一时,SS片选线接低电平即可,从机永远选中,采用三线制通信即可。
SPI通信协议:
起始信号:SS由高到低,是SPI通信起始信号
起始信号:SS由低到高,是SPI通信起始信号
数据传输:SPI采用MOSI和MISO信号线进行数据传输的,使用SCK时钟信号线进行同步。
极性(Clock Polarity ,CPOL) : 决定着时钟起始电平
相位(Clock Phase ,CPHA) :决定采样边沿
SPI有四种通信模式:
当CPHA为0,是sck时钟线为奇数边沿采样
(1)CPOL=0,空闲状态是时钟为低电平
(2)CPOL=1,空闲状态是时钟为高电平
当CPHA为1,是SCK时钟线为偶数边沿采样
(1)CPOL=0,空闲状态是时钟为低电平
(2)CPOL=1,空闲状态是时钟为高电平
模式 CPOL CPHA 采样时刻 空闲时SCK电平
0 0 0 奇数 低
1 0 1 偶数 低
2 1 0 奇数 高
3 1 1 偶数 高
在SPI操作中,最重要的两项设置就是时钟极性(CPOL)和时钟相位(CPHA)这两项即是主从设备间数据采样的约定方式
实验一:LCD显示屏进行单色显示
1.查看原理图
PA15 - PB6
配置CubeMX
RCC - 48M
左侧connective - SPI - Full(全双工主机)
hardware NSS - output
PA15 - SPI_NSS
PB3 - SPI_SCK
PB4 - SPI_MISO
PB5 - SPI_MOSI
PB6 - GPIO_OUTPUT
选好之后去修改时钟分频值 : 4
data size:8(每一个像素点是一个16位数,是由两个八位数字拼接起来的)
生成工程:把源文件粘贴到对应的文件夹下面
.c - src
.h - inc
application/user - 右键添加已经存在的文件
全编译
2.调用函数
Lcd_Init();
Lcd_Clear(YELLOW);
实验二 屏幕显示图片
1. 选择一张图片 - 后缀名修改为xx.bmp(有时候直接改,可能是win11的事,去画图里另存为了修改)
2. 用电脑工具画图 - 打开图片 - 重新调整大小 - 像素 - 128*128 - 保持纵横比去掉 - 保存
3. 图片取模软件 - Image2Lcd 2.9(破解版) - Img2Lcd.exe - C语言数组 水平扫描 16位真彩色 128*128像素 不勾选包含图像头
4. 保存成xx.h的文件 - 粘贴到inc目录下 - 回到工程 #include "xx.h"
5. Lcd_Init(); //初始化
Lcd_Clear(RED); //清屏
HAL_Delay(1000); //延时一短时间
showimage(gImage_xx); //传入图片的数组名
实验三 屏幕显示英文
1. 打开文字取模软件 - PCtoLCD2002.exe
2. 选项 - 阴码 逐行式 C51格式 顺向 16*8(表示生成的字符宽8高16)
根据这种方式就能制作8*16的ASCII码字库
Gui_DrawFont_GBK16(10,10,BLACK,WHITE,"farsight");