STM32学习笔记-基于STM32CubeIDE
本文最后更新于:2022年11月18日 凌晨
参考链接:
- HAL库学习 GPIO—1-知乎
- 【STM32】HAL库 STM32CubeMX系列学习教程-csdn
- STM32CubeMX学习使用(标准库与Cube,LL,直接写寄存器的效率对比)
- STM32 HAL库学习(一):点亮led
- STM32 HAL库学习系列第3篇 常使用的几种延时方式
- 【STM32】HAL库 STM32CubeMX系列学习教程
开发环境:
- 系统:win10
- IDE:STM32CubeIDE
- MCU:STM32F401CCU6
开发板资料: https://pan.baidu.com/s/1UKLarbMPr0GC_aHN9ybCxg 提取码: k7gb
资料下载
学习STM32需要提前准备几份文档资料,在接下来的学习和今后实际运用中都会经常用到。
资料直接从 ST官网 下载即可,有的手册有中文。有的中文手册官网没有,可自行网上搜索下载。但都不推荐使用中文的:版本太老、阅读英文文档是程序员必备技能
- 进入官网 ST官网 ,选择进入
微控制器界面
- 在左侧栏找到自己芯片型号,并进入
Documentation
界面,选择对应文档下载即可。 - 下载以下文档:
- 数据手册:
Arm® Cortex®-M4 32-bit MCU+FPU, 105 DMIPS, 256KB Flash / 64KB RAM, 11 TIMs, 1 ADC, 11 comm. interfacesV11.0
芯片本身的手册,相当于产品说明书。 - 参考手册:
STM32F401xB/C and STM32F401xD/E advanced Arm®-based 32-bit MCUs
,芯片使用参考手册 - 编程手册:
STM32 Cortex®-M4 MCUs and MPUs programming manual
,芯片内核的编程手册(高阶)
- 数据手册:
- HAL和LL库参考手册我们需要另外在官网搜索才能找到,
Description of STM32F4 HAL and low-layer drivers
,库函数API驱动描述手册:
开发环境搭建
参看博文:STM32CubeIDE学习笔记
STM32F030_HAL库学习笔记
操作系统:Win10
硬件平台:STM32F401
软件平台:STRM32CubeIDE V1.6.0
下载器:ST-Link V2
GPIO HAL库 操作与调试
初始化配置
参考上文中的工程模板建立。
程序编写
主要API:
- HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
控制某个具体引脚的状态- GPIO_TypeDef:IO端口编号 GPIOA、 GPIOB、 …、 GPIOG
- GPIO_Pin:IO引脚编号 GPIO_PIN_0…GPIO_PIN_15
- PinState:IO状态 GPIO_PIN_SET 或者 GPIO_PIN_RESET
- HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
- HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
读取某个具体引脚的状态(需要将引脚设置为输入模式GPIO_input
) - HAL_Delay(uint32_t Delay)
ms 级延时函数。IDE内置的,但没有内置 us 级延时函数
主函数:
1 |
|
这里的前两个LED0_GPIO_Port,LED0_Pin
是引脚的宏定义,初始化时系统根据我们在配置界面设置的 IO 别名自动生成的,方便理解,否则没有。请在 main.h 文件中查看。
调试Debug
点击工具栏的 甲虫按钮
在弹出对话框,选择 STM32 CPU
,单击 确认
进入配置界面。在 调试器
下选择 ST-Link 作为调试器,单击 确认
。
在弹出的对话框选择 Switch
,打开调试窗口。
这里就是调试窗口了,红框内的都是和调试有关的工具按钮,这里不多做介绍,自行摸索。
GPIO 寄存器操作
寄存器介绍
BSRR 和 BRR 关系
BSRR 和 BRR 都是 STM32 系列 MCU 中 GPIO 的寄存器。 BSRR 称为端口位设置/清除寄存器,BRR称为端口位清除寄存器。
- BSRR 低 16 位用于设置 GPIO 口对应位输出高电平,高 16 位用于设置 GPIO 口对应位输出低电平。
- BRR 低 16 位用于设置 GPIO 口对应位输出低电平。高 16 位为保留地址,读写无效。
所以理论上来讲,BRR 寄存器的功能和 BSRR 寄存器高 16 位的功能是一样的,都可以控制端口输出低电平。也就是说,输出低电平可以有如下两种写法。
1 |
|
这么来看的话,其实 BRR 寄存器是比较多余的。而实际上,在最新的 STM32F4 系列 MCU 的 GPIO寄存器中,已经找不到 BRR 寄存器了,仅保留了 BSRR 寄存器用于实现端口输出高低电平。
可见,不管是输出高还是输出低,对 BSRR 寄存器的操作最为稳妥。
BSRR常见操作
1 |
|
BSRR还有一个特点,就是如果低6位和高16位同时置1,结果以低16位为准。
就是说同一个bit在 BSRR 低16位中为1(输出高电平),但在高16位中也是1(输出低电平),结果该bit引脚输出 1(高电平)。
此时对多位同时操作可以这么写:
1 |
|
不用考虑哪些需要置1,哪些需要清零
ODR
ODR 寄存器也是用于输出数据的寄存器,一个 ODR 寄存器控制了一组(16位)的 GPIO 输出。因此,对 ODR 进行修改也可以到达对 IO 口输出进行配置。同时通过读取该寄存器,也能够获取 IO 的当前输出状态。而 BSRR 和 BRR 只可写。
但是,由于对 ODR 寄存器的读写操作必须以 16 位的形式进行。因此,如果使用 ODR 改写数据以控制输出时,须采用“读-改-写”的形式进行。
假设需要对 GPIOA_Pin_6 输出高电平。采用改写 ODR 寄存器的方式时,使用“读-改-写”操作,代码如下:
1 |
|
而使用 BSRR 寄存器时,仅需要使用如下语句:
1 |
|
这是因为在修改 ODR 时,为了确保对端口 6 的修改不会影响到其他端口的输出,需要对端口的原始数据进行保存,之后再对端口 6 的值进行修改,最后再写入寄存器。而对 BSRR 的操作,是写 1 有效,写 0 不改变原状态,因此可以对端口 6 置 1,其他位保持为 0。
BSRR 为 1 的话,程序运行时自动会修改相应的 ODR 位。
BSRR、BRR、 ODR 之间的关系
- ODR寄存器可读可写:既能控制管脚为高电平,也能控制管脚为低电平。管脚对于位写1 GPIO管脚为高电平,写 0 为低电平(有被中断打断的风险)
- BSRR 只写寄存器:既能控制管脚为高电平,也能控制管脚为低电平。对寄存器高16位 写1 对应管脚为低电平,写0无动作;对寄存器的第16位写1对应管脚为高电平,写 0 无动作。
- BRR 只写寄存器:只能改变管脚状态为低电平,对寄存器 管脚对于位写 1 相应管脚会为低电平。写 0 无动作。
ODR 能控制管脚高低电平为什么还需要BSRR和SRR寄存器的原因是:用BSRR和BRR去改变管脚状态的时候,没有被中断打断的风险。也就不需要关闭中断,关闭中断明显会延迟或丢失一事件的捕获,所以控制GPIO的状态最好还是用SBRR和BRR。
IDR
GPIO 端口输入数据寄存器。只用了低 16 位。该寄存器为只读寄存器,并且只能以 16 位的形式读出。
要想知道某个 IO 口的状态, 你只要读这个寄存器,再看某个位的状态就可以了。
初始化配置
沿用 GPIO HAL 库操作时的配置
程序编写
寄存器的写法可以通过查看 HAL 函数底层实现,来学习官方如何使用寄存器的。
1 |
|
参考链接
- 高手带你解析STM32 BSRR BRR ODR寄存器
- STM32duino GPIO Registers and programming
- GPIO Output Registers on the STM32
- Would my solution work for 8-bit bus addressing using BSRR and BRR?
- STM32裸机学习笔记(三)—寄存器映射之BSRR与延时的爱恨情仇
- STM32 GPIO 配置之ODR, BSRR, BRR 详解
GPIO 位带操作
位带操作设置
关于位带操作,网上有很多讲解,这里不再详述。可以参考:参考手册
的 GPIO 章节,编程手册
的 2.2.5 Bit-banding 章节,自行深入学习。只需要将下段代码加入到任意头文件中:
1 |
|
上述代码中:
#define BITBAND
后的内容,不同内核可能需要另外修改(M3和M4内核已经验证,可通用)。- 数字
20
和16
是寄存器 ODR 和 IDR 的地址偏移,不同芯片也需要做出相应修改,具体查看参考手册
的 GPIO 章节的 GPIOx_IDR、GPIOx_ODR寄存器描述
图中红框部分 0x10 、0x14 转换成十进制就是 16 和 20。例如 STM32F103 系列的是 0x08、0x0C,那这里就需要改为 12 和 8。
参考链接:
初始化配置
沿用 GPIO HAL 库操作时的配置
程序编写
1 |
|
GPIO DMA 操作
初始化配置
沿用普通给个gpio例程,添加 DMA 配置如下:
DMA 模式Normal
地址增长,只勾选 源地址(也就是内容地址),目标地址不勾选(也就是IO外设地址)
数据宽度设为字节(也可设为其它两个选项,具体再程序编写时说明)
DMA 中断根据需要选择开启
程序编写
1 |
|
source_buffer
存储这引脚状态,每一元素的每一位表示一个一脚,bit0对应pinx-0,依次一一对应。hdma_memtomem_dma2_stream0.XferCpltCallback
是注册回调函数,这里是DMA的普通模式,因此回调函数需要我们自己编写并注册。HAL_DMA_Start_IT
开启DMA传输,若不使用中断也可以使用函数 HAL_DMA_Start 代替。- DMA传输只改变目标地址位宽对应引脚的状态,其它引脚不改变。比如这里 源地址和目标地址位宽都是8位,则最后只会改变pin0-7 引脚状态,其余引脚不受影响。如果是16位则改为pin0-15.
- 如果DMA源地址位宽是8位,目标地址位宽16,则传输数据时,数组的 [0] 和 [1] 共同表示一个引脚状态。
- 不论数组元素是多少位的,传输数据时只传送DMA源地址位宽对应的低位。比如这里数组是32位的,DMA位宽8位,则数组元素只有低8位有效
自定义延时
SysTick介绍
HAL 官方是没有 us 级延时函数的。这里参考正点原子例程,改写了一点。
SysTick定时器是存在于系统内核的一个滴答定时器,只要是ARM Cortex-M0/M3/M4/M7内核的MCU都包含这个定时器,它是一个24位的递减定时器,当计数到 0 时,将从RELOAD 寄存器中自动重装载定时初值,开始新一轮计数。使用内核的SysTick定时器来实现延时,可以不占用系统定时器,由于和MCU外设无关,所以代码的移植,在不同厂家的Cortex-M内核MCU之间,可以很方便的实现。
STM32默认设置 SysTick 定时为1ms,也就是 HAL_Delay 的时钟来源。所以我们无需再初始化,如果是其它芯片,可能需要使用下面语句初始化 SysTick:
1 |
|
下面是具体实现,在新文件中添加以下代码并引用:
1 |
|
初始化配置
参考 HAL 库操作时的 SYS 和 RCC 配置,启动 Timebase Source 和 系统时钟。
程序编写
直接引用即可。
按键输入
这里提供两种按键输入检测方法:阻塞和非阻塞。
硬件原理图
初始化配置
基本沿用GPIO HAL 库操作时的配置,只不过在 IO 功能配置时将按键引脚配置为 输入模式(GPIO_Input),上下拉配置根据硬件选择,这里选择上拉,低电平触发。
阻塞-程序编写
这个就直接参考正点原子的函数即可,利用延时函数消抖延时检测。
1 |
|
非阻塞-程序编写
在编写非阻塞程序前,我们需要先了解一个函数:HAL_GetTick()
如果你仔细研究 HAL_Delay()
函数的话,会发现其实它底层是调用了 HAL_GetTick()
。HAL库中原型如下:
1 |
|
其中的 uwTick
又被HAL_IncTick()
调用,该函数又被系统滴答定时器中断(1ms)调用,每次递增 1, 所以它的值代表了系统上电运行至今的时间(ms),而我们则就可以通过HAL_GetTick()
:
- 获取系统运行时间,最大计时 49.7 天。(
uwTick
为32位,2^32/1000/60/60/24 = 49.7) - 也可以利用该函数,做一个ms的计数器
非阻塞按键检测程序正是利用第2点,移植了 OneButtonLibrary 库,同时实现按键的单击、双击、长按检测。还不会阻塞正常程序执行。
1 |
|
之后只要在主程序中循环调用函数 button_tick()
,读取返回值,就能知道按键的状态。
GPIO 双向 I/O
有时需要IO既要作为输出,还要作为输入读取。如果采用初始化重新配置的话,就会很慢且繁琐。
如果希望某GPIO做双向传输,将其配制为OD输出模式,
F401 开漏输出模式介绍
- 开漏模式:输出寄存器中的“0”可激活 N-MOS,而输出寄存器中的“1”会使端
- 口保持高组态 (Hi-Z)( P-MOS 始终不激活)。
- 施密特触发器输入被打开
- 根据 GPIOx_PUPDR 寄存器中的值决定是否打开弱上拉电阻和下拉电阻
- 输入数据寄存器每隔 1 个 AHB1 时钟周期对 I/O 引脚上的数据进行一次采样
- 对输入数据寄存器的读访问可获取 I/O 状态
- 对输出数据寄存器的读访问可获取最后的写入值
另外其实将IO设置为推挽输出模式时,也可以随时读取 IO 引脚状态,但在该模式下,不论输出高、低电平,P-MOS和N-MOS总有一个处于导通状态,轻则影响外部输入信号,重则烧毁芯片(外部拉低或拉高,MOS都相当于短路,导致大电流)。所以并不能作为双向 IO。
初始化配置
- 将该引脚配置为Output-OpenDrain,
- 在引脚上连接一个上拉电阻(从上图可以看出,F401 能通过软件配置上下拉电阻的;但在 F103 上是没有的,则需要外部硬件上拉)
上述具体实现:在沿用 GPIO HAL 库操作时的配置基础上,只需修改以下图示部分:
程序编写
- 输出时:
1
GPIOx->BSRR = 输出值;
- 输入时: 先输出高电平(否则如果之前输出的是低电平,N-MOS则会导通,影响外部输入),然后通过 GPIOx->IDR 读.
1
LED_GPIO_Port->ODR=(LED_GPIO_Port->ODR | LED_Pin); // 置1
参考链接
GPIO 模拟配置
模拟配置介绍
该模式一般用于复用状态下或低功耗要求下。不作为普通输入输出控制下的模式配置。
总结
1、模拟配置会关闭引脚的一切内部相关联设施,此时普通 I/O 操作失效(不能读也不能输出)。因此引脚功耗为0。因此可以通过将引脚配置为该模式来降低芯片功耗。
2、模拟配置另外好处就是保证了这个引脚是 “干净” 的,如果和外部连接,那个该引脚就完全反映了外部引脚状态。因此将该引脚内联到A/D 片上外设,就可以精确测量引脚的模拟值了。实际上在我们将引脚复用为 A/D 功能时,就会默认配置为 Analog 模式。
初始化配置
Pinout
界面,引脚单击选择即可
或者在 Project Manager
界面选中将不用的引脚都配置为模拟模式,降低功耗
程序编写
无
参考资料
- 1、Question about ADC versus GPIO Analog
- 2、What pins can I use for Analog Input/Output? STM32CubeMX allows every GPIO to be set to ‘’GPIO_Analog’’?
外部中断
通过外部按键,中断触发,再中断函数中翻转LED。
硬件原理图
初始化配置
- 配置引脚为外部中断模式
- 配置引脚:中断触发模式,上下拉。
根据按键原理图,这里设置为上拉,下降沿触发。
- 中断配置:使能中断,中断分组及优先级
程序编写
1 |
|
HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
为 HAL 库的引脚外部中断回调函数,所有的引脚中断都会调用该函数。用户只需要在这里面编写中断处理函数接即可。GPIO_Pin
传参表示触发中断的引脚编号。GPIO_Pin & KEY_Pin
判断当前中断是否由按键引脚触发的,再运行处理函数。
注意: 这个回调函数是只针对外部中断的(EXTI),定时中断和其他中断都都还有自己的回调函数。HAL的思想大概就是同类中断集中在一个回调函数,不同类的分开。
中断与事件
Cortex-M3 处理器内核 vs 基于Cortex-M3的MCU
Cortex-M3 处理器内核是由 ARM 公司设计的,传统意义上的 ARM7/ARM9(简称A7/A9) 也是处理器内核,也是 ARM 公司设计的。
Cortex‐M3处理器内核:故名思意就是单片机(MCU)的核心,是单片机的中央处理单元(CPU)
完整的基于CM3的MCU还需要很多其它组件。在芯片制造商得到CM3处理器内核的使用授权后,它们就可以把CM3内核用在自己的硅片设计中,添加存储器,外设, I/O以及其它功能块。不同厂家设计出的单片机会有不同的配置,包括存储器容量、类型、外设等都各具特色。
中断和异常
中断属于异常的一种。所有能打断正常执行流的事件都称为异常
CM3 的所有中断机制都由 NVIC 实现。除了支持 240 条中断之外, NVIC 还支持 16‐4‐1=11 个内
部异常源(保留了 4+1 个档位),可以实现 fault 管理机制。结果, CM3 就有了 256 个预定义的异常类型。其中编号为 1-15 的对应系统异常,大于等于 16 的则全是外部中断。
类型编号为 1-15 的系统异常如表 7.1 所示(注意: 没有编号为 0 的异常),从 16 开始的外部中断类型如表 7.2 所示
虽然 CM3 是支持 240 个外中断的,但具体使用了多少个是由芯片制造商决定。 CM3 还有一个NMI(不可屏蔽中断)输入脚。当它被置为有效( assert)时, NMI 服务例程会无条件地执行。
STM32外部中断(EXTI )
STM32F103 是基于 CM3 内核设计的,ST 公司(芯片制造商)在原有 CM3 内核基础上,添加了储如定时器、串口、DMA等外设,最终组合成一个STM32单片机。其中 CM3 内核是整个单片机的核心部分,相当于CPU(大脑)
所以 STM32 根据原有 NVIC 中断,从中选择性添加了部分中断,并重新命名与排序。下图是STM32的中断向量表:
从表中可以看出,STM32 对上文中 CM3 内核的系统异常/外部中断表重新进行了编排和删减,把编号从-3 至 6 的中断向量定义为系统异常。从编号 7 开始将原本 CM3 所描述的外部中断又分成了若干中断类型:外部中断(EXTI)、定时器中断、DMA中断等等。
细心的朋友可能已经发现了这里有一个概念冲突:外部中断。释义如下:
CM3 内核描述中的外部中断均是相对于内核而言的,比如串口中断、定时器中断等等都是(内核的)外部中断!而这里提到的STM32的外部中断(EXTI)指的是芯片的外部中断,主要是由芯片外部事件触发的中断,不是内核的外部中断!
STM32的外部中断(EXTI)属于内核的外部中断一部分。在STM32手册中外部中断(EXIT)均是指芯片的外部中断加粗样式,也就是上表中的 EXIT0-9。
这里的内外部
就是物理空间的内外部。
所以当阅读 STM32 参考手册时,外部中断(EXTI)指的均是芯片外部(IO引脚)事件触发的中断。而当阅读网络文章时,则要注意区分。为了避免混淆,都会加 (EXTI)
以区分。
这里还有一个概念:软件中断
,下文中再详述。
另外 STM32 是没有 内部中断 这个概念的,
中断/事件关系
MCU运行过程,其中会有许多各种各样的事件,比方:管脚电平变化、计数器溢出、DMA空、FIFO非空、AD转换结束、超时、外设使能、初始化等等。
其中有些事件本身是不会导致中断产生的,比方外设使能或部分初始化动作是不会导致中断发生的;有些事件则可能导致中断发生,比方计数器溢出,AD转换结束等,这些就是中断事件。当然这些中断事件最终能否触发后续中断,还需要对中断事件进行配置。
先说结论
- 中断:处理器运行的一个状态,该状态会打断处理器当前正常的进程。
- 事件:就是事件。其可能触发中断。
- 中断事件:触发中断的事件,而且软件上也有中断函数的,叫中断事件
- 中断是中断事件发生的结果,中断事件属于事件,事件可分为中断事件或非中断事件
我们可以借助 STM32 MCU的GPIO的外部事件与中断控制器的框图来理解上述结论。
这张图的在 STM32中文手册 中是错误的,英文版的是对的。因而网上很多文章此处的配图都有误,我这里重置了。
我们先关注两个寄存器:中断屏蔽寄存器
和事件屏蔽寄存器
。这两个寄存器决定了从编号1、2、3输入进来的事件最终会输出脉冲发生器(不产生中断)还是 NVIC 中断控制器(产生中断)。从而决定了输入的事件是中断事件还是非中断事件。
MCU参考手册里在谈到事件的触发方式时引入了事件模式
和中断模式
两个概念。这里的不同模式就是通过控制这两个寄存器实现的。
例子:
比方STM32的GPIO口的电平跳变是可能触发外部中断(EXIT)的。但在具体配置时,可以根据需要来决定启用还是禁用相关脚的中断功能,从而选择不同的事件触发方式,即:外部事件模式
和外部中断模式
。如果不希望电平跳变事件触发中断,就配置为事件模式,反之,配置为中断模式
接下来详细说明 EXIT 执行过程。
上图中信号线上划有一条斜线,旁边标志 19字样的注释,表示相同的这样的中断线路共有19条。EXTI中有一个边沿检测电路(编号②)监视着输入线(编号①),并分别与上升沿和下降沿选择寄存器对比。 如果在这两个寄存器中相应的中断线检测开启了,那么当中断线上有上升沿或者下降沿时边沿检测电路就会产生一个事件触发信号给后继的或门。
除了边沿检测电路的输出外,或门(编号 ③)还接受一个软件中断事件寄存器
的输入。 软件中断事件寄存器
的存在使得我们可以通过软件的形式直接触发某一个中断线上的事件。
我们可以通过程序控制此处的
软件中断事件寄存器
,人为的通过或门(编号 ③)输入一个外部事件,从而不需要真实的外部输入,就能产生一个可能触发中断的事件,相当与模拟该中断线上的事件。
诸如ADC、串口、定时器之类产生的中断,就叫
名称+中断
,如:定时器中断、串口中断、ADC中断。并不属于这里的软件中断
范畴,STM32手册中唯一提到软件中断
这个词的就是指这个寄存器,不要混淆了。
或门的输出接到了两个与门(编号 ④、⑤)上,一方面与中断屏蔽寄存器求与编号(④)触发中断, 另一方面与事件屏蔽寄存器求与(⑤)触发事件。 中断屏蔽寄存器控制了相应的中断是否开启了,如果开启了中断将会产生一个中断触发信号,置位中断请求寄存器, 同时将中断触发信号提交给中断控制器(NVIC)。 同样的道理,事件屏蔽寄存器控制事件是否开启,如果开启则直接产生一个脉冲通知后继的功能模块处理事件,例如通知DMA读写内存等。
从这张图上我们也可以知道,从外部激励信号来看,中断和事件的产生源都可以是一样的。之所以分成2个部分,因为中断是需要CPU参与的,需要软件的中断服务函数才能完成中断后产生的结果;但是事件,是靠脉冲发生器产生一个脉冲,进而由硬件自动完成这个事件产生的结果,当然相应的��动部件需要先设置好,比如引起DMA操作,AD转换等;
简单举例: 外部I/O触发AD转换,来测量外部物品的重量;
- 如果使用传统的中断通道,需要I/O触发产生外部中断(EXIT),外部中断(EXIT)服务程序启动AD转换,AD转换完成中断服务程序提交最后结果;
- 要是使用事件通道,I/O触发产生事件,然后联动触发AD转换,AD转换完成中断服务程序提交最后结果;
相比之下,后者不要软件参与启动AD转换,并且响应速度也更块;要是再使用事件触发DMA操作,就完全不用软件参与(AD转换后操作)就可以完成某些联动任务了。
总结:
- 事件触发:机制提供了一个完全由硬件自动完成的触发到产生结果的通道,不要软件的参与,降低了CPU的负荷,节省了中断资源,提高了响应速度(硬件总快于软件),是利用硬件来提升CPU芯片处理事件能力的一个有效方法;
- 中断触发:由软件控制,CPU 参与。
参考链接
- STM32F10xxx参考手册(Reference manual STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced ARM®-based 32-bit MCUs)
- Cortex-M3权威指南(The Definitive Guide to the ARM COrtex-M3)
- Interrupt-Driven Input/Outputon the STM32F407 Microcontroller
- Exceptions and Interrupts——Cuauhtemoc Carbajal-ITESM CEM
- 新手入门之stm32中断系统
- stm32异常、中断和事件的区别
- STM32中断系统(NVIC和EXTI)
- STM32的“外部中断”和“事件”区别和理解
- 【STM32】EXTI—外部中断/事件控制器
- STM32中断与事件
- 浅谈STM32中断模块
- 外部中断(EXTI)控制LED灯
串口
初始化配置
- 配置引脚为串口输入输出模式
- USART配置中选择异步通信模式,并开启串口中断
程序编写
同外部中断类似,串口中断也有自己的中断回调函数,我们再需要的地方编写即可。
主要API:
- HAL_UART_Transmit();串口轮询模式发送,使用超时管理机制,阻塞
- HAL_UART_Receive();串口轮询模式接收,使用超时管理机制,阻塞
- HAL_UART_Transmit_IT();串口中断模式发送,非阻塞
- HAL_UART_Receive_IT();串口中断模式接收,非阻塞
- HAL_UART_TxHalfCpltCallback();一半数据发送完成时调用
- HAL_UART_TxCpltCallback();数据完全发送完成后调用
- HAL_UART_RxHalfCpltCallback();一般数据接收完成时调用
- HAL_UART_RxCpltCallback();数据完全接受完成后调用
- HAL_UART_ErrorCallback();传输出现错误时调用
主函数:
1 |
|
使用步骤:
- 添加以上代码
- 调用
uart_init()
初始化 - 然后通过串口助手发送信息,单片机即返回所发送的信息。
printf,getchar重定义
fgetc,fputc 属于 C 标准可,因此在.ccp 文件中重定义是,需要添加 extern “C” 声明。
1 |
|
使用步骤:
- 添加以上代码
- 包含头文件
#include "stdio.h"
- 添加测试代码:
printf("\n===函数Printf函数发送数据===\n");
测试
打印浮点数
IDE在编译使,默认不支持打印浮点数的(耗费内存和运存)。可以右键单击项目名,在 Properties 中开启该功能:
或者自己编写浮点数打印函数:
1 |
|
串口DMA模式
主要API:
- HAL_UART_Transmit_DMA(); // 使用DMA模式发送数据
- HAL_UART_Receive_DMA(); // 使用DMA模式接收数据
UART以DMA方式接收和发送的函数调用顺序:
循环模式接收:HAL_UART_Receive_DMA()
-> DMA1_Channelx_IRQHandler()
-> HAL_DMA_IRQHandler()
-> UART_DMAReceiveCplt()
-> HAL_UART_RxCpltCallback()
正常模式发送:HAL_UART_Transmit_DMA()
-> DMA1_Channelx_IRQHandler()
-> HAL_DMA_IRQHandler()
-> UART_DMATransmitCplt()
-> USART3_IRQHandler()
-> HAL_UART_IRQHandler()
-> UART_EndTransmit_IT()
-> HAL_UART_TxCpltCallback()
循环发送与正常接收模式与上述类似,不再叙述。这当中还会调用传输 Half 中断,这里也不再讨论了。
以上整个调用过程不需要CPU参与,自动执行。我们只需要关心状态变化即可,无需关心数据怎么传输的。
总结:
对于上述过程,我们只需要知道:DMA 在执行过程中是会调用正常的 USART 的 API 接口函数的就行。也就意味着,如果我们使用了 DMA接收,则不能再用中断接收以及相应的接收函数,否则两者的数据会又冲突,实际测试也是如此。所有的接收过程不应再有软件的参与,我们只需要关系数据是否到来、一半、结束几个标志。同理DMA发送与USART发送函数不可同时使用。特别是在开启了循环模式时。
循环模式
循环模式下,DMA的发送与接收是不断循环的,不会停止。我们只需要在初始化时开启即可。
初始化配置
承接上面 USART 的配置,最初以下修改:
- 在 USART 配置界面中,选中DMA设置
1.1 添加USART_RX/TX 两个通道
1.2 两个通道均选择 循环模式,数据宽度为 1字节 - 在 USART 中断配置界面中,取消串口全局中断
如前文所述,我们开启了DMA的循环模式,为了避免冲突,这里需要关闭串口的全局中断。
软件编写
我们实现串口接收啥就返回啥。和前面的串口功能一样,但这里并不需要软件参与,全程自动执行。
注意: 这里的数组只有 5 个字节,所有只能接收 5 个字节的数据,多了则只保留后5位。
由于发送是自动的,所以下面的程序在接收到数据后,就会不断重复发送该数据,除非有新的外部数据或手动清空。
1 |
|
经测试在双循环模式下,printf也是不能使用的,总之在循环模式下,不要调用任何有关发送和接收数据的函数。
经测试,就算没开启DMA接收,之开启DMA发送,并且数组数据为空,系统仍会不断往外发送数据,但是乱码。所以循环发送模式并不推荐。
正常模式
正常模式下的发送与接收,每一次DMA传输都只会执行一次就接收,如果想要继续使用,就得再手动开启DMA传输。所以如果想要再正常模式下实现自动收发,我们就需要借助中断函数,初始化时下开启 DMA 接收,然后在接收中断中使用DMA发送接收到的数据,并再次启动DMA接收。
初始化配置
在双循环配置基础上,都选择 正常模式(normal)并勾选串口中断。
软件编写
改写之前的串口初始化和接收中断函数,实现功能与前文一样,收啥发啥。
1 |
|
这和一般的串口中断很相似,唯一的区别就是在发送和接收数据时,不需要CPU的参与,仅在这个数据的流动过程是不同的。
经过实际测试,这种模式下,DMA的数据是有问题的。由于DMA发送接收不需要CPU参与,所以在接收中断中调用HAL_UART_Transmit_DMA()
发送串口数据,之后再HAL_UART_Receive_DMA ();
启动接收,整个过程近似无延时,所以当你的数据超过数组长度时,下一次接收是会接收到上一次发送的多余的数据。
有一个解决办法是将 HAL_UART_Receive_DMA ();
放到初始化源码的中断函数里
混合模式
这里用接收循环模式,发送正常模式说明。
初始化配置
在双循环配置基础上,发送选择正常模式,接收选择循环模式。
软件编写
改写之前的串口初始化和接收中断函数,实现功能与前文一样,收啥发啥。
1 |
|
这里的收发数据问题和双正常模式一样,接收数据会记住多余的数据。由于接收数据是自动执行的,随意这里并不能更改修复。
有一个解决办法的思路,就是在发送数据后,清空 DMA 接收缓存,并重置数据指针,但比较麻烦,似乎并不划算。
TIM定时器
初始化配置
这里选择不常用的 TIM10 作为定时器,未提及的沿用 GPIO HAL 库操作时的配置
- 模式(mode)只需勾选使能即可
- 参数配置设置成1KHz定时(系统时钟84MHz)
- 使能定时器中断
程序编写
主要API:
HAL_TIM_PeriodElapsedCallback()
非阻塞模式下经过一段时间的回调HAL_TIM_PeriodElapsedHalfCpltCallback()
在非阻塞模式下,经过了一半的时间完成了回调HAL_TIM_Base_Start_IT()
启动中断模式下的定时器。HAL_TIM_Base_Stop_IT()
停止中断模式下的定时器
主要程序
这里实现定时器中断计数,每1s led翻转 一次。
1 |
|
使用步骤
- 系统初始化时调用
timer_init()
启动定时器 - 在定时器中断中编写处理程序
PWM
初始化配置
- 配置 PA0 引脚为定时器2通道1
- 找到定时器2配置,通道1配置为PWm输出
- 配置定时器参数即PWM参数,周期为1Khz
同一定时器的不同通道使用的PWM频率是一样的
程序编写
主要API
HAL_StatusTypeDef HAL_TIM_PWM_Start()
启动对应通道的PWMHAL_StatusTypeDef HAL_TIM_PWM_Stop()
停止对应通道的PWM__HAL_TIM_SET_COMPARE()
配置对于通道占空比
主程序
/**************** pwm.c *********************/
1 |
|
pwm_init
为初始化函数,启动PWM计数。pwm_test
不断改变通道占空比。定时器中断中调用该函数,实现计数变化。
硬件上,将PA0和LED端口相连。
实现功能:LED亮度会逐渐变暗(10S一周期)
使用步骤:
- 在系统初始化函数中调用 pwm_init 初始化
- 使用
__HAL_TIM_SET_COMPARE
控制指定通道的占空比输出
输入捕获
我们通过输入捕获计算按键按下低电平的时间
初始化配置
- 引脚配置,IO配置为上拉
- 定时器配置
捕获频率1Mhz,计数周期最大(能够测量更多时间,防止溢出,当然程序也做了一处处理)
由于外部按键时低电平有效,所以这里选择下降沿捕获
程序编写
主要API
HAL_TIM_IC_Start_IT();
启动输入捕获HAL_TIM_IC_CaptureCallback()
输入捕获中断回调函数__HAL_TIM_SET_CAPTUREPOLARITY();
在运行时设置定时器输入捕获极性。HAL_TIM_PeriodElapsedCallback()
定时器溢出中断回调函数__HAL_TIM_SET_COUNTER();
在运行时设置TIM计数器寄存器的值。HAL_TIM_ReadCapturedValue()
从捕获比较单元读取捕获的值,其实就是捕获中断发生时的定时器计数值
主程序
1 |
|
- 先初始化捕获中断为下降沿触发,当下降沿触发后,立即设置为上升沿触发,保存来个那次触发时的定时器计数值,在和定时器频率 1MHz 计算就能得出低电平的总时间。
- 溢出中断则计算溢出的次数,防止低电平时间过长当时计数器溢出。
- 主程序循环判断捕获标志位,打印输出时间
IWDG
独立看门狗(IWDG)由专用的低速时钟(LSI)驱动(40kHz),即使主时钟发生故障它仍有效。独立看门狗适合应用于需要看门狗作为一个在主程序之外 能够完全独立工作,并且对时间精度要求低的场合。
独立看门狗只适用于系统死机的情况,如果某个程序异常,但系统仍能正常喂狗,此时独立看门狗时不会起作用的。
如果需要检测某个程序段是否正常,使用窗口看门口狗,后续会单独讲解。
初始化配置
- 配置PA0为GPIO输入模式,上拉。作为后面的按键检测
- IWDG 使能,配置时钟分频和重装载值
IWDG的超时时间 Tout = (42^prv) / LSI * rlv (s) prv是预分频器寄存器的值,rlv是重装载寄存器的值
根据时钟图分析
LSI 为 25 KHz,当 prv 取 IWDG_ PRESCALER_64 ,rlv 取 500 时,Tout=64/32500=1s。
程序编写
主要API
HAL_IWDG_Refresh(&hiwdg)
刷新看门狗(喂狗)
主程序
看门狗不需要额外初始化,上电即运行,所以要注意一点,如果系统的初始化时间过长,应该及时喂狗。
建议使用定时器定时喂狗,且最先初始化定时器主程序不断检测按键电平值,由于按键默认上拉,所以系统会每 800 ms喂狗一次(超时溢出为 1 秒),此时系统正常1
2
3
4
5
6
7
8
9
10
11
12
13
14void setup() {
uart_init();
printf("\n\r***** IWDG Test Start *****\n\r");
}
void loop()
{
printf("\n\r Refreshes the IWDG !!!\n\r");
if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) != 0){
HAL_IWDG_Refresh(&hiwdg);
}
delay_ms(800);
}
当我们按下按键不放时,程序停止喂狗,会看到系统会1s重启一次(根据打印的数据查看系统状态)
WWDG
窗口看门狗跟独立看门狗一样,也是一个递减计数器不断的往下递减计数,当减到一个固定值 0x3F 时还不喂狗的话,产生复位,这个值叫窗口的下限,是固定的值,不能改变。
窗口看门狗之所以称为窗口,就是因为其喂狗时间是在一个有上下限的范围内(窗口上限值~下限值0x3F),在这个范围内才可以喂狗,可以通过设定相关寄存器,设定其上限时间(但是下限是固定的0x3F)
图中:
- 数字 1 处为计数器的初始值(重装载值)
- 2 是我们设置的窗口上限值(只能取低7为值,也就是最大值为 127)
- 3 是下窗口值(0x3F, 那么W[]最小为64)
当窗口看门狗计数器的值只有处在 2 和3 之间(上窗口和下窗口之间)才可以喂狗,其余时间喂狗都时异常。
窗口看门狗还可以使能提前唤醒中断,如果系统出现问题,喂狗函数没有生效,那么在计数器由减到0x40 (0x3f+1) 的时候,便会先进入提前唤醒中断,之后才会复位,你也可以在该中断里面喂狗(不建议在中断里喂狗,不然效果和独立看门狗类似,无意义)
窗口看门狗的超时公式如下:Twwdg=(4096× 2^WDGTB× (T[5:0]+1)) /Fpclk1;
其中:
- Twwdg: WWDG 超时时间(单位为 ms)(看门狗的计数周期)
- Fpclk1: APB1 的时钟频率(单位为 Khz)注意看门狗时钟靠在PCLK1下,一般为主时钟一半。
- WDGTB: WWDG 的预分频系数(系数范围[0-3],2^WDGTB = 分频值)
- T[5:0]:窗口看门狗的计数器低 6 位(0-64)
根据前面所述,你的 W[]只能取 64-127,则计数器T[]的值范围为 0-63。假设 Fpclk1=42Mhz,分频值为8,W[] = 127(T[] = 63),则看门狗计数周期:
T = 4096*8*[63+1]/42000 = 50ms
初始化配置
- 开启看门狗中断、配置参数(超时时间 = 4096*8*[63+1]/42000 = 50ms)
- 开启提前唤醒中断
- 开启中断
程序编写
主要API
HAL_WWDG_Refresh()
看门狗喂狗HAL_WWDG_EarlyWakeupCallback()
看门狗提前唤醒中断
主程序
1 |
|
我们在唤醒中断里不断喂狗(实际使用时不建议在中断里放喂狗函数,这里应放置整个系统故障的 “临终遗嘱”)。
通过串口助手的时间戳显示,两条信息 WWDG well!
之间的时间约为 50 ms。如果注释掉喂狗函数,系统就会不断重启。
待机唤醒
STM32 的低功耗模式有 3 种:
- 1)睡眠模式(CM3 内核停止,外设仍然运行)
- 2)停止模式(所有时钟都停止)
- 3)待机模式(1.8V 内核电源关闭)
在运行模式下,我们也可以通过降低系统时钟关闭 APB 和 AHB 总线上未被使用的外设的时钟来降低功耗。
在这三种低功耗模式中,最低功耗的是待机模式。停机模式是次低功耗的。最后就是睡眠模式了。
这里将对 STM32 的最低功耗模式-待机模式做介绍
STM32 进入及退出待机模式的条件
我们有使用WKUP 引脚上的上升沿 方式退出待机模式。从待机唤醒后,除了电源控制/状态寄存器(PWR_CSR), 所有寄存器被复位。从待机模式唤醒后的代码执行等同于复位后的执行(采样启动模式引脚,读取复位向量等)。电源控制/状态寄存器(PWR_CSR)将会指示内核由待机状态退出。
初始化配置
配置PA0(WKUP 引脚)为输入下拉。
程序编写
主要API
__HAL_RCC_PWR_CLK_ENABLE()
使能 PWR 时钟HAL_PWR_EnableWakeUpPin()
设置 WKUP 用于唤醒HAL_PWR_EnterSTANDBYMode()
设置 SLEEPDEEP 位,设置 PDDS 位,执行 WFI 指令,进入待机模式。__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU)
清除Wake_UP标志
主程序
1 |
|
PA0 按键用来唤醒待机模式,并使用串口1打印相关调试信息
系统运行时倒计时,3秒钟后进入待机模式。当 PA0 接高电平时,待机模式被唤醒,系统重新运行,重新倒计时。
低功耗模式下载 Debug 需要 reset 按键手动复位
ADC
ADC时钟
挂靠在 PCLK2(APB2时钟,最大84HHz)下。分频因子可配置2/4/6/8分频
ADC转换周期:
1 |
|
例如,当 ADCCLK=14Mhz 的时候,并设置 1.5 个周期的采样时间,则得到: Tcovn=1.5+12.5=14 个周期=1us。
根据芯片数据手册,电气特性(Electrical characteristics)-> 操作条件(Operating conditions)所述:
由 表6.3.1 可知,ADC的最大采样速率(转换速率)与VDDA有关,当VDDA低于2.4V时,转换速率最大只有1.2Msps(million samples per second);而当VDDA高于2.4V时,可达2.4Msps,即每秒一百二十万次转换。
无论是1.2Msps还是2.4Msps,都是相对于12位分辨率来说的,即表14中给出的是最高分辨率(12bit)下的最大转换速率。STM32F4系列MCU支持12位、10位、8位和6位可编程分辨率,更低的分辨率可以缩短转换周期。因此采用降低分辨率的方法还可以进一步获得更大的转换速率。
由 表6.3.20 可知,ADC的最大时钟频率在VDDA低于2.4V时为18MHz,VDDA高于2.4V时为36MHz。
对于12位分辨率来说,转换周期为12个ADC周期,采样时间可编程的最小值为3个ADC周期,即12位分辨率的最少转换周期数为15个ADC周期。
因此,当VDDA低于2.4V时12位分辨率的最大转换速率为 18/15 Msps,即上面提到的1.2Msps。当VDDA高于2.4V时12位分辨率的最大转换速率为 36/15 Msps,即上面提到的2.4 Msps。
为了保证ADC转换结果的准确性,ADC的时钟最好不超过14M。(有的stm32单片机最高只支持1MHz转换速率)
参考链接
- STM32F4系列ADC最大转换速率及操作条件(以STM32F407ZGT6为例)
ADC的转换模式 (重要,请务必看懂)**
1 单次转换模式:ADC只执行一次转换,转换完成后,必须再手动开启
2 连续转换模式:转换结束之后马上开始新的转换,每次转换结束,ADC的值会被刷新,所以需要及时读出数据;
3 扫描模式:ADC扫描被规则通道和注入通道选中的所有通道,在每个组的每个通道上执行单次转换。在每个转换结束时,这一组的下一个通道被自动转换。
4 间断模式:触发一次,转换一个通道,在触发,在转换。在所选转换通道循环,由触发信号启动新一轮的转换,直到转换完成为止。
扫描模式简单的说是一次对所有所选中的通道进行转换,比如开了ch0,ch1,ch4,ch5。 ch0转换完以后就会自动转换通道1,4,5直到转换完这个过程不能被打断。如果开启了连续转换模式,则会在转换完ch5之后开始新一轮的转换。
间断模式,可以说是对扫描模式的一种补充。它可以把0,1,4,5这四个通道进行分组。可以分成0,1一组,4,5一组。也可以每个通道单独配置为一组。这样每一组转换之前都需要先触发一次。
单通道、多通道配置
ADC单通道:
只进行一次ADC转换:配置为“单次转换模式”,扫描模式关闭。ADC通道转换一次后,就停止转换。等待再次使能后才会重新转换
进行连续ADC转换:配置为“连续转换模式”,扫描模式关闭。ADC通道转换一次后,接着进行下一次转换,不断连续。
ADC多通道:
只进行一次ADC转换:配置为“单次转换模式”,扫描模式使能。ADC的多个通道,按照配置的顺序依次转换一次后,就停止转换。等待再次使能后才会重新转换
进行连续ADC转换:配置为“连续转换模式”,扫描模式使能。ADC的多个通道,按照配置的顺序依次转换一次后,接着进行下一次转换,不断连续。
也就是:多通道必须使能扫描模式
数据左对齐或右对齐
因为ADC得到的数据是12位精度的,但是数据存储在 16 位数据寄存器中,所以ADC的存储结果可以分为左对齐或右对齐方式(12位)
ADC输入通道
从ADCx_INT0-ADCx_INT15 对应三个ADC的16个外部通道,进行模拟信号转换 此外,还有两个内部通道:温度检测或者内部电压检测
选择对应通道之后,便会选择对应GPIO引脚,相关的引脚定义和描述可在开发板的数据手册里找
注入通道,规则通道
我们看到,在选择了ADC的相关通道引脚之后,在模拟至数字转换器中有两个通道,注入通道,规则通道,
规则通道至多16个,注入通道至多4个
规则通道:
规则通道相当于你正常运行的程序,看它的名字就可以知道,很规矩,就是正常执行程序
注入通道:
注入通道可以打断规则通道,听它的名字就知道不安分,如果在规则通道转换过程中,有注入通道进行转换,那么就要先转换完注入通道,等注入通道转换完成后,再回到规则通道的转换流程
无法连续转换注入通道。连续模式下唯一的例外情况是,注入通道配置为在规则通道之后自动转换(使用 JAUTO 位),请参见自动注入一节
中断
中断触发条件有三个,规则通道转换结束,注入通道转换结束,或者模拟看门狗状态位被设置时都能产生中断,
转换结束中断就是正常的ADC完成一次转换,进入中断,这个很好理解
模拟看门狗中断
当被ADC转换的模拟电压值低于低阈值或高于高阈值时,便会产生中断。阈值的高低值由ADC_LTR和ADC_HTR配置
模拟看门狗,听他的名字就知道,在ADC的应用中是为了防止读取到的电压值超量程或者低于量程
DMA
同时ADC还支持DMA触发,规则和注入通道转换结束后会产生DMA请求,用于将转换好的数据传输到内存。
注意,只有部分ADC组可以产生DMA请求
因为涉及到DMA传输,所以这里我们不再详细介绍,之后几节会更新DMA,一般我们在使用ADC 的时候都会开启DMA 传输。
单通道单次转换
初始化配置
- 配置引脚为ADC1_IN1
- 使能通道1
- 匹配ADC参数
这里根据上描述设置ADC时钟为6分频,14MHz。其余配置保持默认即可,也不要修改
参数讲解
- ADC_Mode_Independent 这里设置为独立模式
独立模式模式下,双ADC不能同步,每个ADC接口独立工作。所以如果不需要ADC同步或者只是用了一个ADC的时候,应该设成独立模式,多个ADC同时使用时会有其他模式,如双重ADC同步模式,两个ADC同时采集一个或多个通道,可以提高采样率 - Data Alignment (数据对齐方式): 右对齐/左对齐
- Scan Conversion Mode( 扫描模式 ) : DISABLE
如果只是用了一个通道的话,DISABLE就可以了(也只能DISABLE),如果使用了多个通道的话,会自动设置为ENABLE。 就是是否开启扫描模式 - Continuous Conversion Mode(连续转换模式) DISABLE
设置为ENABLE,即连续转换。如果设置为DISABLE,则是单次转换。两者的区别在于连续转换直到所有的数据转换完成后才停止转换,而单次转换则只转换一次数据就停止,要再次触发转换才可以进行转换 - Discontinuous Conversion Mode(间断模式) DISABLE
多通道模式下使用 - Enable Regular Conversions (启用常规转换模式) ENABLE
使能 否则无发进行下方配置 - Number OF Conversion(转换通道数) 1
用到几个通道就设置为几,数字大于1即多个通道会自动使能上面的扫描模式 - Extenal Trigger Conversion Source (外部触发转换源)
设定ADC的触发方式,外部事件触发时使用。详见中断与事件
章节 - Regular Conversion launched by software 规则的软件触发 调用函数触发即可
上述时触发ADC,这个时触发ADC的规则处理,如:启动下一次转换 - Rank 转换顺序
这个只修改通道采样时间即可 默认为1.5个周期。其余配置在多通道再讲解。
HAL库中关于个参数的讲解:
1 |
|
程序编写
主要API
HAL_ADC_Start();
启动ADC转换__HAL_ADC_GET_FLAG()
查询标志位,判断ADC状态HAL_ADC_GetValue()
获取ADC值HAL_ADC_PollForConversion(&hadc1, 50)
等待转换完成,第二个参数表示超时时间,单位ms.HAL_ADC_GetState(&hadc1)
换取ADC状态,HAL_ADC_STATE_REG_EOC表示转换完成标志位,需配合HAL_ADC_PollForConversion()
使用。
主程序
1 |
|
系统初始化组后,启动ADC转换。主程序不断查询ADC转换状态,如果就绪,就打印ADC值并重新启动下一次转换。
网上很多教程会使用函数
1 |
|
查询ADC状态,但必须配合函数
1 |
|
使用,这种方案使用阻塞查询,也就是启动转换后,会等待转换完成,会阻塞程序,不推荐使用。详细使用参考上述程序对应注释部分。
网上教程还会有校准函数
1 |
|
这个依芯片而异,这里使用的 F401 就没有。
开启ADC 3种模式 ( 轮询模式 中断模式 DMA模式 )
- HAL_ADC_Start(&hadcx); //轮询模式开启ADC
- HAL_ADC_Start_IT(&hadcx); //中断轮询模式开启ADC
- HAL_ADC_Start_DMA(&hadcx); //DMA模式开启ADC
关闭ADC 3种模式 ( 轮询模式 中断模式 DMA模式 )
- HAL_ADC_Stop()
- HAL_ADC_Stop_IT()
- HAL_ADC_Stop_DMA()
单通道单次转换-中断方式
初始化配置
紧跟上文的配置,只需要再使能中断即可。
程序编写
主要API
HAL_ADC_Start_IT()
中断模式下开启ADC转换HAL_ADC_Stop_IT()
停止准换HAL_ADC_ConvCpltCallback()
中断模式下转换完成后回调,DMA模式下DMA传输完成后也会调用
主程序
1 |
|
系统初始化组后开启ADC中断转换,中断回调函数里读取当前ADC数据并打印出来。其中的开关全局中断为可选项,是为了避免其它中断打断这里的ADC数据读取,这里只有一个中断,并未开启。
多通道单次转换
STM32的多通道是没有多通道值存储寄存器的,也就是说在普通轮询模式下,我们只能通过函数
1 |
|
读取一个通道的值,并不能一次性读取多个通道,STM32每次只能转换一个通道。
随意我们在开启多通道时,读取数值的基本方法就是转换一次,读取一次,在开启转换,在读取。
初始化配置中能帮我们解决的只有:不用手动切换通道以及切换通道的重新初始化配置。
初始化配置
在单通道单次转换配置基础上修改:
- 使能多个通道
- 参数配置
参数详解
- Scan Conversion Mode( 扫描模式 ) : ENABLE
如果使用了多个通道的话,会自动设置为ENABLE。 - Discontinuous Conversion Mode(间断模式) ENABLE
根据数据手册描述:大致意思就是,要是没有使用DMA,那么管理多个通道(转化序列)转换就需要使用软件处理,需开启间断模式(EOCS = 1),每次转换后,转换完成标志为(EOC = 1),和单次一样,通过判断这个标志位读取ADC值,之后再手动开启转换,系统就会自动开始转换下一个组。依次循环往复(最后一组转换完成会自动跳转到第一组)如果我们不开启间断模式,那么系统就会一次性转换所有组,那么我们在读取时也就只能读到最后一个通道的值。1
2
3
4
5
6
7
8在不使用 DMA 的情况下管理转换序列
如果转换过程足够慢,则可使用软件来处理转换序列。在这种情况下,必须将 ADC_CR2 寄
存器中的 EOCS 位置 1,才能使 EOC 状态位在每次转换结束时置 1,而不仅是在序列结束
时置 1。当 EOCS = 1 时,会自动使能溢出检测。因此,每当转换结束时, EOC 都会置 1,
并且可以读取 ADC_DR 寄存器。溢出管理与使用 DMA 时的管理相同。
要在 EOCS 位置 1 时将 ADC 从 OVR 状态中恢复,请按以下步骤操作:
1. 将 ADC_SR 寄存器中的 ADC OVR 位清零
2. 触发 ADC 以开始转换。 - Number of Discontinuous Conversion 1
定义序列长度,距离分组模式。
比如我们一共有 1、3、4、5、6 通道
这里设为1,则每个通道为一组,那么每一个通道转换完成后都会置位 EOC。
如果设备2,这、则 1、3一组,4、5一组、6一组,那么没两个通道转换完成后,才会置位EOC,那么其实我们也就只能读取到每组最后一个通道的ADC值。 - Number OF Conversion(转换通道数) 3
用到几个通道就设置为几,数字大于1即多个通道会自动使能上面的扫描模式 - Rank 转换顺序
这里是设置通道的转换顺序,Ranx 系统是按照x的顺序开始转换的。
例如如果这里我们 Rank1设为通道3,Rank2设为通道4,依次设置,则转换顺序为 3、4、5、6、1.
如果我们的序列长度为2,则3、4一组,5、6一组。系统会先转换3、4通道,按序执行。而不是1通道先转换。
这里我们序列长度设为1,转换顺序为1、3、5.则按一般顺序转换,没完成一个通道,EOC置位1次。
程序编写
1 |
|
初始化需先开启依次转换,不然我们的 adc1_scan()
中判断会不通过。
主程序不断调用 adc1_scan()
扫描每个通道,并将值存储到数组,在一次性读取打印出来。
在 adc1_scan()
我们无需关心通道的配置,系统会按照我们初始化配置是的顺序自动轮询转换。
不然我们就需要像下面这要,手动切换通道并初始化:
1 |
|
这里还要解答一个疑问点:
既然 Number of Discontinuous Conversion
序列长度大于1时,我们就无法读取部分通道的值,那么这个设置是做什么的呢?
数据手册中也有解答:
1 |
|
意思就是并不是每个ADC通道的值都需要我们读取,有的可能转换完成后会用于其它目的(模拟看门狗)。比如 1、3通道是用于模拟看门狗的,那么我们的实际程序是不需要知道这两个通道的值,系统需要,我们也就不需要读取。这是序列为2后,系统会自动在每个通道转换完成后获取通道值并用于模拟看门狗比较。在这一组转换完成后,EOC置1,告诉我们这两个通过转换完成。
同样连续转换模式 Continuous Conversion Mode
如果再没开启DMA情况下,我们手动读取数据,也只能读取到最后一个通道的转换结果。所以一般模式下,不是能该选项。
DMA转换-连续模式-circle
DMA转换的好处就是无需手动获取查询转换状态吗,再手动保存通道值,DMA会自动将数据放进数组中。
初始化配置
在上面多通道单次转换基础上修改:
- 添加一路DMA,配置参数:循环模式,字节为 World(必须为字,因为ADC变量是32位的)
- 配置ADC参数
这里DMA中断时强制开启的,无法关闭。DMA循环模式会一致更新数据到数组中,如果改为Normal模式,则需要在 ADC 转换完成回调函数中手动启动下一次 DMA 转换。(DMA中断最终会调用ADC的所有回调函数,即使没启动ADC中断)
程序编写
主要API
- HAL_ADC_Start_DMA(&hadc1, ();
开启ADC DMA转换1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32/******************* adc.c ***************/
uint32_t adcBuf[3] = {0};
void adc1_start()
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adcBuf, 3); // 开启ADC DMA转换
}
// 获取ADC值
// ch 通道索引 注意这个值并不直接对应实际通道号,仅仅是数组的索引,对应通道存入数组的顺序
// 例如本次共开启了1、3、4通道,并在 adc1_scan() 中依次存入了 adcBuf 中
// 则 ch=1 对应通道1;ch=2 对应通道3
uint16_t adc1_get(uint8_t ch)
{
return adcBuf[ch];
}
/********************** main.c *******************/
void setup() {
uart_init();
printf("Test Start\n\r");
adc1_start();
}
void loop()
{
for(int i = 0;i<3;i++){
printf("adc1_CH%d = %d\n\r",i,adc1_get(i));
}
delay_ms(1000);
}
系统初始化开启 DMA 传输即可,后面不用再管了,主程序值负责读取即可。
如果你选择了DMA 的 Normal 模式,则需要再中断里手动开启下一次转换。
DMA-不连续模式-circle
该模式和 DMA ,DMA在一轮转换完成后会停止,需要手动再次开启下一轮转换。
初始化配置
只在上述配置中将 Continuous Conversion Mode 改为 Disable。不是使能连续转换。
程序编写
1 |
|
系统初始化开启 DMA 传输即可,中断里手动开启下一次转换。
主程序读取ADC值并打印。
DMA-不连续模式-Normal
该模式和 DMA-不连续模式-circle 很像。
这两者差别目前并不清楚。推测时应用场景不同,连续模式下,所有通道转换完成才会触发中断,不连续模式下,每个通道转换完成触发依次中断。
初始化配置
只在上述配置中将 Continuous Conversion Mode 改为 Disable。DMA 改为 Normal 模式
注意, Continuous Conversion Mode 必须改为 Disable。否则数组中的数据会发生偏移,比如,通道1数据第一次会存储在 adcBuf[0],下一次转换再读取,数据就会存储再adcBuf[1],其它通道数据依次移动,然后再循环往复。
从实验结果来看,无论是不是能连续模式,每次DMA转换都会转换所有的通道,所以连续在这的意义即使是数据偏移???
参考链接:STM32使用HAL库的ADC多通道数据采集(DMA+非DMA方式)+ 读取内部传感器温度
程序编写
1 |
|
系统初始化开启 DMA 传输即可,中断里手动开启下一次转换。
主程序读取ADC值并打印。
其它
关于 ADC在DMA模式下的 DMA Continuous Request
作用,目前仍无结论。下面是可能有帮助的参考:
温度传感器可用于测量器件的环境温度 (TA)。
- 对于 STM32F40x 和 STM32F41x 器件,温度传感器内部连接到 ADC1_IN16 通道,而
ADC1 用于将传感器输出电压转换为数字值
主要特性
- 支持的温度范围: —40 °C 到 125 °C
- 精度: ±1.5 °C
使用以下公式计算温度:
1 |
|
VSENSE 位电压值 v。
有关 V25 和 Avg_Slope 实际值的相关信息,请参见数据手册中的电气特性一节。
一般典型值为:V25 = 0.76; Avg_Slope = 2.5mv/℃
注意: 传感器从掉电模式中唤醒需要一个启动时间,启动时间过后其才能正确输出 VSENSE。 ADC 在
上电后同样需要一个启动时间,因此,为尽可能减少延迟间,应同时将 ADON 和 TSVREFE
位置 1。
温度传感器的输出电压随温度线性变化。由于工艺不同,该线性函数的偏移量取决于各个芯片(芯片之间的温度变化可达 45 °C)。
内部温度传感器更适用于对温度变量而非绝对温度进行测量的应用情况。如果需要读取精确温度,则应使用外部温度传感器。**
初始化配置
在 多通道单次转换
基础上修改
- 使能内部温度传感器
- 配置adc参数:通道数改为4,rank4选择内部温度通道。
程序编写
程序和 多通道单次转换
一样,将adcBuf改为4就行了。主程序修改一下:
1 |
|
单独将通道16的数值转换位温度值并打印输出。我这初始输出温度 31,比环境温度高不少(环境15度左右),可见内部温度如手册所说,不准确。用手触摸芯片,会发现温度在升高。
DAC
STM32F401 不支持 DAC,暂空。
IIC(EEPROM)
IIC DMA
SPI(Flash)
使用SPI驱动外部 flash 芯片(W25q128)。
初始化配置
- 配置PA4引脚位输出,作为片选
- 开启spi,参数默认
程序编写
主要API
HAL_SPI_TransmitReceive();
在阻塞模式下发送和接收大量数据。HAL_SPI_Receive()
在阻塞模式下接收大量数据。HAL_SPI_Transmit()
在阻塞模式下发送大量数据。
先编写两个 spi 底层读写接口。共flash函数使用。
1 |
|
然后是 Flash 的驱动函数.
w25qxx.h 主要是一些宏定义和指令表,不详细列出,具体请查看源码,另外定义了片选引脚(位带操作)。
1 |
|
w25qxx.c 就是主要的 flash 操作函数了。这里也不在=列出,详细请查看源码。
我们直接看主程序:
1 |
|
我们在初始化配置时先检测 flash ID型号,判断是否初始化成功。
随后写入一串字符,之后读取并打印出来。如果打印内容与字符串内容一致,说明 falsh 操作成功。
SPI DMA
flash读写会占用比较长的时间 ,如果这个SPI上还挂载了其他SPI 器件,如SPI显示屏,就需要通过开启DMA来提升速度了。
初始化配置
承接上文 SPI 配置,添加以下几项
- 开启DMA通道,可以只开一个
- 配置DMA参数
- 关闭SPI中断
程序编写
主要API
HAL_SPI_Transmit_DMA()
使用DMA在非阻塞模式下传输大量数据。HAL_SPI_Receive_DMA()
使用DMA在非阻塞模式下接收大量数据。HAL_SPI_TransmitReceive_DMA()
使用DMA在非阻塞模式下发送和接收大量数据。HAL_SPI_GetState()
返回SPI句柄状态。HAL_SPI_TxCpltCallback()
SPI发送完成回调函数,DMA中断调��HAL_SPI_RxCpltCallback()
SPI接收完成回调函数,DMA中断调用
主程序
在 SPI 的基础上,修改两个函数:SPI的读与写
1 |
|
这里的 SPI DMA操作实际还是阻塞模式,每次传输完成必须使用 while 检查 falsh状态,才能开启下一次传输,否则可能会导致只一次还未结束,有开始下一轮数据传输(DMA非阻塞,与主程序并行),所以主程序和DMA传输数据直接存在交叉现象,可以添加标志位知识DMA传输状态,但和此处的while效果一样,最终还是需要等待每一次数据传输完成才能开始下一次。
这里使用DMA的唯一好处就是读写速度加快,虽然主程序会等待 DMA 完成,但 数据的传输过程不需要 CPU 参与,所以同一数据使用DMA的速度更快,也就是这里的等待时间更短。
根据网上他人测试结论:DMA速度是普通(基于HAL库)的 3倍,另外使用寄存器方式也会比 HAl 库快三倍,如果使用 DMA+寄存器 方式就会比 HAL快9倍
内部Flash模拟EEPROM
不同型号的STM32F4xC/E,其FLASH容量也有所不同,最小的只有256K字节,最大的512K字节。STM32F401的FLASH容量为256K字节,STM32F411xC/E产品的闪存模块组织如图所示:
STM32F4的闪存模块由:主存储器、系统存储器、OPT区域和选项字节等4部分组成。
主存储器,该部分用来存放代码和数据常数(如const类型的数据)。分为8个扇区,前4个扇区为16KB大小,然后扇区4是64KB大小,扇区5~7是128K大小,不同容量的STM32F411拥有的扇区数不一样,比如我们的STM32F411RCT6,则拥有6个扇区,从上图可以看出主存储器的起始地址就是0X08000000, B0、B1都接GND的时候,就是从0X08000000开始运行代码的。
系统存储器,这个主要用来存放STM32F4的bootloader代码,此代码是出厂的时候就固化在STM32F4里面了,专门来给主存储器下载代码的。当B0接V3.3,B1接GND的时候,从该存储器启动(即进入串口下载模式)。
OTP区域,即一次性可编程区域,共528字节,被分成两个部分,前面512字节(32字节为1块,分成16块),可以用来存储一些用户数据(一次性的,写完一次,永远不可以擦除!!),后面16字节,用于锁定对应块。
选项字节,用于配置读保护、BOR级别、软件/硬件看门狗以及器件处于待机或停止模式下的复位。
闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制机构。
在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行;既在进行写或擦除操作时,不能进行代码或数据的读取操作。
STM23F4的FLASH读取是很简单的。例如,我们要从地址addr,读取一个字(一个字为32位),可以通过如下的语句读取:data=*(vu32*)addr;
将addr强制转换为vu32指针,然后取该指针所指向的地址的值,即得到了addr地址的值。类似的,将上面的vu32改为vu8,即可读取指定地址的一个字节。相对FLASH读取来说,STM32F4 FLASH的写就复杂一点了,下面我们介绍STM32F4闪存的编程和擦除。
闪存的编程和擦除
执行任何Flash编程操作(擦除或编程)时,CPU时钟频率 (HCLK)不能低于1 MHz。如果在Flash操作期间发生器件复位,无法保证Flash中的内容。
在对 STM32F4的Flash执行写入或擦除操作期间,任何读取Flash的尝试都会导致总线阻塞。只有在完成编程操作后,才能正确处理读操作。这意味着,写/擦除操作进行期间不能从Flash中执行代码或数据获取操作。
STM32F4的闪存编程由6个32位寄存器控制,他们分别是:
- FLASH访问控制寄存器(FLASH_ACR)
- FLASH秘钥寄存器(FLASH_KEYR)
- FLASH选项秘钥寄存器(FLASH_OPTKEYR)
- FLASH状态寄存器(FLASH_SR)
- FLASH控制寄存器(FLASH_CR)
- FLASH选项控制寄存器(FLASH_OPTCR)
STM32F4复位后,FLASH编程操作是被保护的,不能写入FLASH_CR寄存器;通过写入特定的序列(0X45670123和0XCDEF89AB)到FLASH_KEYR寄存器才可解除写保护,只有在写保护被解除后,我们才能操作相关寄存器。
FLASH_CR的解锁序列为:
- 写0X45670123到FLASH_KEYR
- 写0XCDEF89AB到FLASH_KEYR
通过这两个步骤,即可解锁FLASH_CR,如果写入错误,那么FLASH_CR将被锁定,直到下次复位后才可以再次解锁。
STM32F4闪存的编程位数可以通过FLASH_CR的PSIZE字段配置,PSIZE的设置必须和电源电压匹配,见表:29.1.2:
由于我们开发板用的电压是3.3V,所以PSIZE必须设置为10,即32位并行位数。擦除或者编程,都必须以32位为基础进行。
STM32F4的FLASH在编程的时候,也必须要求其写入地址的FLASH是被擦除了的(也就是其值必须是0XFFFFFFFF),否则无法写入。STM32F4的标准编程步骤如下:
- 检查FLASH_SR中的BSY位,确保当前未执行任何FLASH操作。
- 将FLASH_CR寄存器中的PG位置1,激活FLASH编程。
- 针对所需存储器地址(主存储器块或OTP区域内)执行数据写入操作:
—并行位数为x8时按字节写入(PSIZE=00)
—并行位数为x16时按半字写入(PSIZE=01)
—并行位数为x32时按字写入(PSIZE=02)
—并行位数为x64时按双字写入(PSIZE=03) - 等待BSY位清零,完成一次编程。
按以上四步操作,就可以完成一次FLASH编程。不过有几点要注意:1,编程前,要确保要写如地址的FLASH已经擦除。2,要先解锁(否则不能操作FLASH_CR)。3,编程操作对OPT区域也有效,方法一模一样。
我们在STM32F4的FLASH编程的时候,要先判断缩写地址是否被擦除了,所以,我们有必要再介绍一下STM32F4的闪存擦除,STM32F4的闪存擦除分为两种:扇区擦除和整片擦除。
扇区擦除步骤如下:
- 检查FLASH_CR的LOCK是否解锁,如果没有则先解锁
- 检查FLASH_SR寄存器中的BSY 位,确保当前未执行任何FLASH操作
- 在FLASH_CR寄存器中,将SER位置1,并从主存储块的12个扇区中选择要擦除的
扇区 (SNB) - 将FLASH_CR寄存器中的STRT位置1,触发擦除操作
- 等待BSY位清零
经过以上五步,就可以擦除某个扇区。本章,我们只用到了STM32F4的扇区擦除功能,整片擦除功能我们在这里就不介绍了.。
通过访问内部 falsh 地址及其内容,达到类似 eeprom 的读写操作。
目前测试:访问内部数据会得到数据并打印出来,但之后程序死机。原因未知
初始化配置
在上文 SPI(Flash)
基础上编写,不用额外初始化。
程序编写
新建 stmflash.c文件,用于操作stm32的内部flash
1 |
|
该部分代码,我们重点介绍一下STMFLASH_Write函数,该函数用于在STM32F4的指定地址写入指定长度的数据,该函数的实现基本类似第24章的SPI_Flash_Write函数,不过该函数对写入地址是有要求的,必须保证以下两点:
- 该地址必须是用户代码区以外的地址。
- 该地址必须是4的倍数。
- 对OTP区域编程也有效。
第1点比较好理解,如果把用户代码给卡擦了,可想而知你运行的程序可能就被废了,从而很可能出现死机的情况。不过,因为STM32F4的扇区都比较大(最少16K,大的128K),所以本函数不缓存要擦除的扇区内容,也就是如果要擦除,那么就是整个扇区擦除,所以建议大家使用该函数的时候,写入地址定位到用户代码占用扇区以外的扇区,比较保险。
第2点则是3.3V时,设置PSIZE=2所决定的,每次必须写入32位,即4字节,所以地址必须是4的倍数。第3点,该函数对OTP区域的操作同样有效,所以大家要写OTP字节,也可以直接通过该函数写入,不过注意OTP是一次写入的,无法擦除,所以,一般不要写OTP字节。
然后打开stmflash.h,该文件代码代码非常简单,我们就不做介绍了。
最后,打开main.c文件,main函数如下:
1 |
|
主函数部分代码非常简单,首先先进行写操作,然后再读。至此,我们的软件设计部分就结束了。
这里要提醒以下:
主函数的u32 datatemp[SIZE];
定义在正点原子的程序是u8 datatemp[SIZE];
,而我们在读操作时
1
>STMFLASH_Read(FLASH_SAVE_ADDR,(u32*)datatemp,SIZE);
用的是32位的,正点原子的例程在读取函数这里将8位强制转换为32位,这在keil上面是没问题(正点原子例程是用的keil)的。但是在 STM32CubeIDe 中这么使用则是错误的,会导致读操作时造成硬件报错
HardFault_Handler
。所以这里在一开始定义就要定义为32位的。
参考链接
- STM32F407.FLASH 读写经验
- STM32F4内部Flash读写
- STM32F030 Read/Write Flash Throwing HardFault
- STM32 Hardfault exception when writing globally declared buffer to FLASH
内存管理
使用标准库
void *
在进行下面话题之前,我们先回忆一下 void * 是什么?
void * 表示未确定类型的指针。C/C++规定,void * 类型可以强制转换为任何其它类型的指针。
void * 也被称之为无类型指针,void * 可以指向任意类型的数据,就是说可以用任意类型的指针对 void * 赋值,如下示例:
1 |
|
但一般不会反过来使用,如下示例在有些编译器上面可以编译通过,有些就不行:
1 |
|
可以修改一下代码,将 void * 转换为对应的指针类型再进行赋值,如下示例:
1 |
|
由于 GNU 和 ANSI 对 void * 类型指针参与运算的规定不一样,所以为了兼容二者并且让程序有更好的兼容性,最好还是将 void * 转换为有明确类型的指针再参与运算,如下示例。
1 |
|
malloc
void * malloc(size_t size);
malloc 向系统申请分配指定 size 个字节的内存空间,即 malloc 函数用来从堆空间中申请指定的 size 个字节的内存大小,返回类型是 void * 类型,如果成功,就会返回指向申请分配的内存,否则返回空指针,所以 malloc 不保证一定成功。
另外需要注意一个问题,使用 malloc 函数分配内存空间成功后,malloc 不会对数据进行初始化,里边数据是随机的垃圾数据,所以一般结合 memset 函数和 malloc 函数 一起使用。
1 |
|
free
void free(void *ptr);
free 函数会释放指针指向的内存分配空间。
对于 free 函数我们要走出一个误区,不要以为调用了 free 函数,变量就变为 NULL 值了。本质是 free 函数只是割断了指针所指的申请的那块内存之间的关系,并没有改变所指的地址(本身保存的地址并没有改变)。如下示例:
1 |
|
正确且安全的做法是对指针变量先进行 free 然后再将其值置为 NULL,如下下面示例:
1 |
|
calloc 函数
void * calloc(size_t count, size_t size);
在堆上,分配 n*size 个字节,并初始化为0,返回 void *类型,返回值情况跟 malloc 一致。
函数 malloc() 和函数 calloc() 的主要区别是前者不能初始化所分配的内存空间,而后者能。如果由 malloc() 函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之,如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据。也就是说,使用 malloc() 函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题。
函数 calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;如果你为实型数据分配内存,则这些元素会被初始化为浮点型的零。
realloc() 函数
void * realloc(void *ptr, size_t size);
realloc() 会将 ptr 所指向的内存块的大小修改为 size,并将新的内存指针返回。假设之前内存块的大小为 n,如果 size <= n,那么截取的内容不会发生变化,如果 size > n,那么新分配的内存不会被初始化。
对于上面说的新的内存指针地址可能变也可能不变,假如原来alloc的内存后面还有足够多剩余内存的话,realloc后的内存=原来的内存+剩余内存,realloc还是返回原来内存的地址即不会创建新的内存。假如原来alloc的内存后面没有足够多剩余内存的话,realloc将申请新的内存,然后把原来的内存数据拷贝到新内存里,原来的内存将被free掉,realloc返回新内存的地址。
另外要注意,如果 ptr = NULL,那么相当于调用 malloc(size);如果 ptr != NULL且size = 0,那么相当于调用 free(ptr)。
当调用 realloc 失败的时候,返回NULL,并且原来的内存不改变,不会释放也不会移动。
示例
1 |
|
参考链接
自写malloc库
- 栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。
- 堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于数据结构中的链表。
stm32cubeide 默认配置
1 |
|
0x00000400 等于1024字节所以等于1K
0x00000200 等于512字节所以等于512 Byte
由于 malloc()
分配的动态内存在堆区域,因此调大堆空间 Heap_Size
为 0xC00,即 3072 字节大小。用户可以自由使用的堆空间,大约为这里分配的堆总空间的一半。超过时系统就会死机,也就是 3072/2 字节可以被用户用来自由使用。
STM32CUBEIDE——malloc
STM32分配堆栈空间不足问题原因及解决方法
USB虚拟串口
STM32向PC发送的是USB协议的数据包,跟串口自身没有关系
PC端的USB接口收到USB协议的数据包后,由驱动程序来解包并放入操作系统的串口缓冲区里,这样,串口助手类的工具就能够从缓冲区里读到数据,串口助手就认为是有 uart数据到来了。
所以虚拟串口和串口不是一个概念,本质也不同,那么其实串口的参数配置对虚拟串口来说也就没用了。在我们链接虚拟串口测试时,无论怎么更改串口助手的波特率,都不会影响数据接收和发送。
初始化配置
开发板已经将芯片的USB引脚接到了 micro 接口上,我们只需要用数据线连接电脑和开发板即可。这里的 USBDP/DM
引脚是直接和usb接口时直连的,不需要一般串口需要接一个串口芯片。
使能USB接口
参数保持默认,speed 参数设置通信速度,可自行再尝试修改;引脚再使能后系统自动选择配置,和我们的硬件一致,无需更改。
配置USB模式位虚拟串口
参数保持默认。其中的USB CDC Rx Buffer Size 是定义接收数组大小,下面的是发送数组大小,可以尝试修改。
程序编写
USB 虚拟串口的API我们常用的对外接口都在 usbd_cdc_if.c/.h 文件中。其中主要API:
CDC_Receive_FS()
接收数据。CDC_Transmit_FS()
发送的数据CDC_TransmitCplt_FS()
数据发送完成回调函数
以上三个函数都在usbd_cdc_if.c文件中,一般需要需改是 CDC_Receive_FS()
函数,用来处理接收的数据
主程序
初始化配置后,我们无需任何修改,就可以直接发送数据。主函数如下:
1 |
|
通多usb连接
数据接收
需要更改一下原来的CDC_Receive_FS()
函数:
1 |
|
我们仅添加了
1 |
|
实现数据的回传。
printf 功能
通过自定义一个printf函数,实现与串口中的重定向printf 功能。
在 usbd_cdc_if.c
添加如下内容:
1 |
|
然后在.h 中声明一下。
1 |
|
最后主程序中直接调用即可:
1 |
|
参考链接:
- STM32 usb虚拟串口 最大速度可以达到多少 波特率可以设置到多少?
- 使用STM32CubeMX把USB配置成虚拟串口(virtual com port)
- 使用STM32CubeMX实现USB虚拟串口的环回测试功能
- stm32Cubemx USB虚拟串口
- STM32使用虚拟串口CDC重定向printf
- STM32CubeMX之USB从机
- STM32 USB虚拟串口波特率问题(含源码)
- STM32CubeMX实现STM32的USB虚拟串口功能
USB-U盘
将外部flash W25Qxx 作为U盘,通过电脑可以像访问U盘一样访问 falsh 里的数据。类似U盘
初始化配置
在之前的spi驱动,读写 W25Q256 基础上配置。
使能USB接口
参数保持默认,speed 参数设置通信速度,可自行再尝试修改;引脚再使能后系统自动选择配置,和我们的硬件一致,无需更改。
配置USB模式,选择大容量存储设备。读写扇区大小改为4096
程序编写
主要修改 usbd_storage_if.c
w文件内容,添加 SPi 的读写接口到 USb读写中。
在原有的宏定义下,重新定义,应为如果我们直接修改的话,下次初始化就又会被 IDE 该回来。
- 定义扇区数 = 1024x8 扇区,内存大小 =扇区数x扇区大小 = 102484096 = 32M。这个数字是根据 W25Q256 = 32M,倒退计算得来的。
- 定义块大小,必须和我们初始化配置时的一致。要改一起改,而且要和上面的计算配合,不然实际程序虽然也能用,但显示的内存容量就会不一样。
这里的扇区大小和芯片的扇区是两个概念,这里的扇区指 USB协议下的 U盘设备的扇区大小,所以通常比芯片的扇区大很多。
1 |
|
然后修改连个读写接口:
将SPI读写API添加进来。其余不许改动,之后直接使用即可。
1 |
|
主函数:
只需要初始化 W25Q256 接可。其实在原有程序上完全不用改动。下面的程序还是原来 SPI 读写的程序。
1 |
|
然后使用 USB 现连接开发板和电脑,随后电脑会弹窗提示格式化,点确认
U盘初始化配置保持和下图一致即可,然后点击 开始
格式化完成
之后就想一般u盘操作,保存读取文件即可。(注意文件不要太大,就32M)。下图是存放的图片,重新插拔USB线,图片依旧还在,也能正常读取,删减。测试成功.
参考连接
stm32USB之模拟U盘
stm32 cubemx usb spi flash w25q128 u盘调试笔记
用STM32F0系列内部Flash虚拟出U盘
stm32使用外部SPI FLASH模拟U盘(大容量存储设备MSC)求职学习资料
cubemx配置 USB读卡器+FATFS
Fatfs 文件系统移植
再上述模拟u盘基础上,添加文件系统。这样就可以使用usb线实现将内存存储到flash中,再通过文件系统识别读取flash中的文件。
初始化配置
再上述 USB-U 盘章节基础上修改
- 勾选使能FATFS
- 配置FATFS
- 支持长文件名并将缓存放在 STACK(栈)中
- 最大扇区(MAX_SS)修改为4096
- 缓存工作区为什么放在栈?其实fatfs提供了三个选项:BSS,STACK , HEAP,根据个人情况选一个。
- 在BSS上启用带有静态工作缓冲区的LFN,不能动态分配。
- 如果选择了HEAP(堆)且自己有属于自己的malloc就去重写ff_memalloc ff_memfree函数。如果是库的malloc就不需要。
- 一般都选择使用STACK(栈),能动态分配。
- 当使用堆栈作为工作缓冲区时,请注意堆栈溢出。Stack Size只要不溢出就行。
3、为什么最大扇区大小是4096Byte?一般别人都是512Byte? 其实这个是根据你自己使用的存储芯片和驱动相关的。因为我使用的W25Q128这款芯片是最小擦除单位是4096。不使用512byte是因为效率大大降低但是优点是空间利用率会大大提高。比如你文件系统最大分区是512,但是芯片最小擦除单位是4096,那么你在驱动就要实现先用缓存区把整个扇区4096byte全部读出来,然后判读其中写入512byte中有没有擦除过(即全0xFF),没有的话先擦除,在把数据写入缓存区最后写入芯片。所以步骤繁琐效率低,但是优点就是存储空间的利用率会大大提高,避免太多浪费。
另外可能需要修改堆栈大小。
程序编写
我们需要先配置底层函数接口,在 user_diskio.c
文件中
1 |
|
之后编写主程序测试:
1 |
|
下载程序并运行。程序会查找flash中的文件 name.txt
没有则会创建一个空白文件。我们使用usb连接电脑,会发现falsh中多了一个FatFs_test.txt
文件,我们打开该文件,手动添加一些内容(不能是中文,中文支持后面再说明)保存,重启,会观察到串口输出了文件内容,表示测试成功。
fatfs文件系统中文支持
IAP
STM32的内部闪存(FLASH)地址起始于0x08000000,一般情况下,程序文件就从此地址开始写入。此外STM32是基于Cortex-M3内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是0x08000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
常规程序运行状态:
STM32在复位后,先从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main函数,如图标号②所示;而我们的main函数一般都是一个死循环,在main函数执行过程中,如果收到中断请求(发生重中断),此时STM32强制将PC指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回main函数执行,如图标号⑤所示。
IAP程序运行流程:
STM32复位后,还是从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到IAP的main函数,如图标号①所示,此部分同图33.1.1一样;在执行完IAP以后(即将新的APP代码写入STM32的FLASH,灰底部分。新程序的复位中断向量起始地址为0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main函数,如图标号②和③所示,同样main函数为一个死循环,并且注意到此时STM32的FLASH,在不同位置上,共有两个中断向量表。
在main函数执行过程中,如果CPU得到一个中断请求,PC指针仍强制跳转到地址0X08000004中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回main函数继续运行,如图标号⑥所示。
通过以上两个过程的分析,我们知道IAP程序必须满足两个要求:
- 新程序必须在IAP程序之后的某个偏移量为x的地址开始;
- 必须将新程序的中断向量表相应的移动,移动的偏移量为x;
初始化配置
沿用前面的前面的 GPIO HAL库操作(LED闪烁)和 串口 两个程序,不修改初始化配置。
程序编写
我们有2个程序,一个为Bootloader即IAP程序,一个是app即主程序。
- Bootloader程序负责接收串口发送过来的app程序,并存储到flash中,再实现程序跳转,运行已经存储的app程序。
- app程序就是我们要运行的主程序,也是IAO升级要修改的程序,以后更新该程序时可以直接通过串口发送更新。
app程序
app程序直接使用GPIO HAL库操作(LED闪烁)程序,main 函数修改如下:
1 |
|
注意这里修改的时 Src 文件夹下初始化生成main函数。这里只在程序开始处添加了一行SCB->VTOR = FLASH_BASE | 0x8000;
,用于中断向量表偏移量的设置。这里的 0x8000
一定要大于我们的 Bootloader程序
占用的flash大小,这个需要我们在编写完Bootloader程序
后,编译执行才能看到,从下图得知本次的Bootloader程序
falsh占用 17.68KB,而 0x8000
约为 32KB,满足要求。
还要修改一下程序flash链接地址。打开 STM32Fxxxx_FLASH.ld 文件,在开头部分找到下图所示内容
将falsh起始地址改为0x08008000,大小改为 224KB(256KB-32KB)。
修改好后,重新编译,会默认生成 bin 文件,在项目文件夹下的 Debug
文件夹中。留好备用。
Bootloader程序
Bootloader程序直接使用串口程序,首先修改串口接收回调函数
Bootloader程序部分大概思路:
- 先将Bootloader程序通过stlink烧录到MCU
- 运行MCU,Bootloader程序开始会循环检测串口有没有数据,如果在一段时间后仍没有数据过来,Bootloader程序将会执行跳转,运行旧的app程序
- 如果等待期间有数据过来,Bootloader程序将通过串口接收APP文件,利用数组先保存下来存储到USART_Buffer中,然后再写入flash,最后执行跳转,运行新的app程序(刚从串口接收的app)
1 |
|
回调函数没啥特别的,主要讲解 USART_RX_BUF[USART_REC_LEN]
的定义,正点原子使用 __attribute__ ((at(0X20001000))) ={0}
,其作用就是把变量或函数绝对定位到 Flash 或者 RAM 中,区别就是后面的地址,写成0x80000000就是定位到falsh中,写成0x20000000就是定位到RAM中。
这里我们选择定位到 RAM 中,一般用于数据量比较大的缓存,如串口的接收缓存,再就是某个位置的特定变量。用于用于串口发送过来的app程序。
但以上语句只能在keil编译器(基于MDK)中使用,而在STM32CubeIDE(基于GCC)是不支持的,所以我们需要修改为:__attribute__((section(".myBufSection"))) USART_RX_BUF[USART_REC_LEN]
,同时还需要再次修改link文件,打开 STM32Fxxxx_FLASH.ld 文件,将
1 |
|
添加到 SECTIONS
处,如下图所示。
具体含义及使用参考以下链接:
- GCC编译器对变量绝对定位怎么写?
- Defining Variables at Absolute Addresses with gcc
- GNU Linker, can you NOT Initialize my Variable?
- C语言__attribute__的使用
这里有个bug,这里使用的 STM32F401 单片的RAM空间只有64KB,而flash空间有256KB,所以如果我们的app程序大64KB,就无法一次性存储到 RAM 中了,必须采用边接收边写入的方式,这里我们的app程序只有5.76Kb,数组设置为10K,完全够用。
然后修改主函数如下:
1 |
|
程序一开始会循环等待 20*200ms,查看串口是否有新的程序发送过来,有的话就接收并存储到 RAM 中,接收完成后,在写入到falsh中,然后跳转执行新的app程序。如果没有新程序发过来,在等待时间结束后,程序仍会跳转执行app程序(旧的)。
使用:
- 将Bootloader程序下载至MCU,上电运行。
- 通过串口助手发送之前编译好的APP的bin文件,等待写入flash,直至完成。
- 等待跳转到APP运行。
参考资料:
- STM32 IAP 升级设计(HAL)
- stm32cubeide iap
- STM32CubeMx生成的工程中使用Printf函数调试和IAP(在线下载功能)
- STM32F4 IAP学习笔记
- STM32CUBEIDE IAP跳转失败,求助?!!
- STM32CubeIDE IAP原理讲解,及UART双APP迭代升级IAP实现
- STM32 Cube IDE 下实现 IAP —— (1) 程序跳转
LCD驱动
参考资料
硬件配置
本次驱动采用8位并口驱动,无触摸(pin1、2、3、4不接),DB0-7可以悬空不用接地。
初始化配置
在 串口 例程基础上修改。主要是添加以下 IO 引脚。
- PB0- 7作为数据引脚接硬件DB8-15
- PB10 LCD背光(实际未接)
- PB12 RESET
- PB14 CS
- PC13 RD
- PC14 WR
- PC14 RS
程序编写
一般 TFTLCD 模块的使用流程:
第一个是 LCD_WR_DATA 函数,该函数在 lcd.h 里面,通过宏定义的方式申明。该函数通
过 并口向 LCD 模块写入一个 16 位的数据,使用频率是最高的,这里我们采用了宏定义的方
式,以提高速度,实际测试刷屏速度比放在 .c 文件中块 33ms 。其代码如下
1 |
|
第二个是: LCD_WR_DATAX 函数,该函数在 ILI93xx.c 里面定义,功能和 LCD_WR_DATA
一模一样。宏定义函数的好处就是速度快(直接嵌到被调用函数里面去了),坏处就是占空
间大。在 LCD_Init 函数里面,有很多地方要写数据,如果全部用宏定义的 LCD_WR_DATA 函
数,那么就会占用非常大的 flash,所以我们这里另外实现一个函数: LCD_WR_DATAX,专门
给 LCD_Init 函数调用,从而大大减少 flash 占用量。
该函数代码如下:
1 |
|
第三个是 LCD_WR_REG 函数,该函数是通过 8080 并口向 LCD 模块写入寄存器命令,因
为该函数使用频率不是很高,我们不采用宏定义来做(宏定义占用 FLASH 较多),通过 LCD_RS
来标记是写入命令(LCD_RS=0)还是数据(LCD_RS=1)。该函数代码如下:
1 |
|
LCD_WriteReg 用于向 LCD 指定寄存器写入指定数据,
1 |
|
坐标设置函数。
1 |
|
画点函数。先设置坐标,然后往坐标写颜色。其中 POINT_COLOR 是我们
定义的一个全局变量,用于存放画笔颜色。LCD_DrawPoint 函数虽然简单,但是至关重要,其他几乎所有上层函数,都是通过调用这个函数实现的。
1 |
|
字符显示函数 LCD_ShowChar,字符显示函数多了一个功能,就是可以以叠加方式显示,或者以非叠加方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。
该函数实现代码如下:
1 |
|
在 LCD_ShowChar 函数里面,我们采用快速画点函数 LCD_Fast_DrawPoint 来画点显示字符,该函数同 LCD_DrawPoint 一样,只是带了颜色参数,且减少了函数调用的时间,详见本例程源码。 该代码中我们用到了三个字符集点阵数据数组 asc2_2412、 asc2_1206 和 asc2_1608,都存放在 lcd_font.h 文件中,点阵数据的提取方式该文件中有详细描述。
最后是我们的主函数测试程序:
1 |
|
先初始化,然后画线,清屏,再显示字符。
ST7789 SPI 四线
当改为 spi 串口驱动时,需要注意屏幕FPC后方的模式选择硬件接口(有的屏幕没有则不需要关注)。
当前使用的屏幕时需要通过调整电阻接线方式来选择屏幕的数据接口位数的。
当前屏幕出厂默认接口方式如下:
可以发现接线时默认接了 R1、R3、R5的,也就是默认16位驱动。如果我需要SPI串口驱动吗,应该改为R2、R4、R6焊接(此时PIN38-40无用)或者接R7、R8、R9(通过Pin38-40任意调整接口模式)。这里选择后者,这样就可以通过改变外部引脚接线方式改变接口形式,而不需要频繁焊接电阻。
硬件配置
本次驱动采用SPI串口驱动,无触摸(pin1、2、3、4不接),DB0-15可以悬空不用接地。
注意屏幕背面的电阻(<10R)要仅焊接 R7-9,其余留空。
初始化配置
沿用上一节并口驱动例程;
- 去除并口例程中的IO驱动引脚配置
- PA0-2 设置位输出引脚
- 添加SPI驱动,配置如下图
- SPI_SCK PA5 接 SCL
- SPI_MOSI PA7 接 SDA
- SPI_MISO PA6 接 SDO
- PB10 LCD背光(实际未接)
- PA0 接 RESET
- PA1 接 CS
- PA2 接 RS
参考资料
程序编写
与并口区别主要就是底层数据输出方式,主要函数改动如下:
1 |
|
其余初始化及上层画图函数保持不变。
另初始化时要是能SPI
1 |
|
ILL9488 SPI驱动
ILL9488 SPI模式下:数据必须是24位的即RGB666模式。
同时 ILI9488 芯片手册Display Data Format
章节中在对SPI的数据数据描述中也仅用3bit和18bie两种形式
一般的颜色数据都是RGB565格式即16位的,所以在使用时需要将16位转为24位的再输出才能正确控制LCD屏。
1 |
|
在驱动时间上是16位的1/3倍。而后期使用的LVGL图形库只支持16位或32位格式的颜色,并不支持24位数据,所以无法是哟个
总之不推荐使用 9488 的屏幕,请使用 ST7796 的替代,此系列可使用16位的SPI模式。
引用外网一句原话:
1 |
|
1 |
|
参考资料
- ili9488还不赖
- 有用过ILI9488的RGB接口的朋友吗?
- jaretburkett/ILI9488
- Ili9488 & lvgl?
- Due + ILI9488 SPI - Problem
- Need sample code for ILI9488 LCD on SPI Interface
- a bug in the ILI9488 driver?
- 3.5寸TFT液晶屏 ILI9488 mcu spi 触摸屏并口/串口模块原子ips全视角液晶屏 显示屏
硬件配置
本次驱动采用SPI串口驱动,无触摸(pin1、2、3、4不接),DB0-15可以悬空不用接地。
初始化配置
沿用 ST7789-SPI 的配置,仅改变接线方式
- SPI_SCK PA5 接 SCL
- SPI_MOSI PA7 接 SDA
- SPI_MISO PA6 接 SDO
- PB10 LCD背光(实际未接)
- PA0 接 RESET
- PA1 接 CS
- PA2 接 RS
程序编写
基本和ST7789-SPI驱动类似,不过如开始所述,数据输出要转位24位,所以改动如下;
- 初始化函数改动,这里不再列出,直接看源码即可
注意RGB格式:ILI默认是 BGR 格式,所以 ST7789_MADCTL_RGB 设为 0x08.而ST7789默认是 RGB,随意设为 0x00。 - 数据写入时改为24位:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//画点
//x,y:坐标
//POINT_COLOR:此点的颜色
void LCD_DrawPoint(u16 x,u16 y)
{
LCD_SetCursor(x,y); //设置光标位置
LCD_WriteRAM_Prepare(); //开始写入GRAM
LCD_WriteRAM(POINT_COLOR);
}
//快速画点
//x,y:坐标
//color:颜色
void LCD_Fast_DrawPoint(u16 x,u16 y,u16 color)
{
LCD_SetCursor(x,y); //设置光标位置
LCD_WriteRAM_Prepare(); //开始写入GRAM
LCD_WriteRAM(POINT_COLOR);
}
虽然数据接口是24位的,但是在初始化写寄存器时,仍可使用16位,这也就是为什么其它函数未修改的原因。24位格式仅影响 FRAM 的数据输入(即屏幕内部数据缓存),不影响寄存器写入。
参考资料
- LVGL-DemoTest
- ILI9341(new)SPI library for Due supporting DMA transfer(Uno, Mega,.. compatible)
- Adafruit_ILI9341
- Adafruit-GFX-Library
LCD DMA
经实际测试与理论分析:
- DMA可以实现从内容到GPIO的数据控制
- 并口且为普通IO控制时,不适合使用DMA(每传一个数据就要控制WR引脚跳变,就需要没一个字节产生一个DMA中断,程序实现复杂,且速度并无显著提升)
- 并口且为FSMC接口控制是,适合DMA操作(通过FSMC写数据,WR等引脚会自动跳变,无需程序参数,只负责传数据即可,时序由系统自动控制)
- SPI串行接口时,适合DMA,原因同 FSMC 接口。
参考资料
- Connecting a parallel LCD using ST7789 with STM32H743VI
- STM32 and ILI9341 16bit Parallel
由于这里没有FSMC接口芯片,暂不编写。只编写 SPI 接口的 DMA 例程。
另由于 ILI9488 SPI 数据位24位,而DMA位16位或32位,故该屏的DMA例程不再编写,也不推荐使用该驱动芯片的屏。
ST7789_SPI_DMA
硬件配置
硬件配置参考前面的 ST7789 SPI 四线
章节内容。
初始化配置
初始化配置基本参考前面的 ST7789 SPI 四线
章节内容。仅修改下面内容部分:
- 添加 SPI TX DMA
- 数据位宽 16 位
- SPI中断关闭,DMA中断默认开启
程序编写
这里只编写清屏函数,因为DMA传输实际是要和地址挂钩的(外置Flash存储的图片地址或内部图片数组地址),这里仅演示DMA用法,具体的实际使用在后续例程中在讲解。
1 |
|
我们定义了一个数组 SendBuff
作为数据缓存,并将颜色值写入进去,在开始前我们先将spi的位数转为 16 位,因为DMA的数据位宽是16位的,而LCD初始化配置时是8位的,所以需要切换。
1 |
|
然后再开启一轮 DMA 传输,这里该送入数据SendBuff时改为8位,是HAl库的一个bug,但是经对函数内部分析,实际执行时,会将把这两个指针重新变换为( uint16_t *) 。这里8位,但实际还是16位的数据,所以数据宽度仍按16位的计算。
之后会在DMA中断中置位传输完成标志为,循环里检测该标志位,发送下一轮DMA数据,知道一屏幕的数据发送完成,才跳出循环,从而完成刷屏
主程序如下:
1 |
|
这里主程序一个使用DMA刷屏,一个SPI刷屏。可以直观的对比两者速度差异。
这里也可以再配置是将数据位宽改为8位的,不过测试就需要修改以下,主要就是DMA发送数据是的宽度x2,SPI的配置也不要才来回在 8 位和16位之间切换了。
1 |
|
参考资料:
- 基于stm32 标准库spi驱动st7789(使用DMA)
- STM32使用SPI DMA加双缓冲区的方式加速LCD显示BMP图片时刷屏速度
- cubemx spi 中断_STM32HAL库SPI的16位数据中断发送与接收
- STM32F103的GPIO与DMA的终极(没啥用)玩法
- Using Direct Memory Access (DMA) in STM32 projects
- Particle Photon (STM32F205) DMA Control of GPIO pins
- TM32 DMA输出到GPIO问题
- STM32并口数据通过DMA传输
LCD 内部字符显示
使用软件将字符变为数组,存储在内存中,然后调用。
ASCII 字符
取模
常用ASCII表
偏移量32 (这句话不知道什么意思)
ASCII字符集(注意首位有个空格不要忘记复制):
1 |
|
打开PC2LCD2002
模式设为:字符模式
选项设置:
- 阴码+逐列式+顺向+C51格式
- 这里的后缀尽量保持一致,影响最终数组的输出排版和注释
- 点阵:输出的字模每行字节数。大小 = (size/8+((size%8)?1:0))*(size/2),size:点阵大小(12/16/24…)。根据计算,12点阵这里应该填写12,但实际测试只要比计算结果大就可以,该值影响数组的输出排版。可以保证每个字符的字符组输出在同一大括号内,不然就会分两行、两个大括号显示,不利于复制使用。
- 索引:每次字模生成在开始时都会有一个索引,但我们不会复制使用这段索引。这里的值也表示每行显示的索引个数,这里任意。
- 选择字体:字宽和字高值和上面的点阵大小保持一致
- 在界面下方输入栏复制粘贴开头的字符集(注意空格)。点击生成字模,然后将字符复制保存,粘贴到程序文件中。
- 在程序文件中新建数组,名称自定义 数字 95 是固定的,表示95个字符。数字 12 表示字符所占的字节数(也就是下面的行元素个数),计算公式和步骤2一样:
1
2
3
4
5
6const unsigned char asc2_1206[95][12]={ // 6x12 宋体
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*" ",0*/
{0x00,0x00,0x00,0x00,0x3E,0x40,0x00,0x00,0x00,0x00,0x00,0x00},/*"!",1*/
{0x00,0x00,0x20,0x00,0xC0,0x00,0x20,0x00,0xC0,0x00,0x00,0x00},/*""",2*/
...
};
大小 = (size/8+((size%8)?1:0))*(size/2)
12点阵就是12,23点阵就是36.
程序编写
1 |
|
主程序:
1 |
|
中文字符
取模
打开PC2LCD2002
模式设为:字符模式
选项设置:
- 阴码+逐列式+顺向+C51格式
- 这里的后缀尽量保持一致,影响最终数组的输出排版和注释
- 点阵:输出的字模每行字节数。大小 = (size/8+((size%8)?1:0))*(size),size:点阵大小(12/16/24…)。根据计算,12点阵这里应该填写12,但实际测试只要比计算结果大就可以,该值影响数组的输出排版。可以保证每个字符的字符组输出在同一大括号内,不然就会分两行、两个大括号显示,不利于复制使用。
- 索引:每次字模生成在开始时都会有一个索引,但我们不会复制使用这段索引。这里的值也表示每行显示的索引个数,这里任意。
- 选择字体:字宽和字高值和上面的点阵大小保持一致
- 然后在输入栏输入汉字,点击 “生成字模”,生成的字模如下
- 然后将字模复制到程序文件中,新建数组
注:数组为一维数组。每个字的字模前需要添加对应的汉字
1
2
3
4
5
6
7
8
9
10
11
12
13
14typedef struct
{
unsigned char Index[2];
unsigned char Msk[24];
}typFNT_GB12;
const typFNT_GB12 tfont12_cn[] = {
"中",0x00,0x00,0x1F,0x80,0x11,0x00,0x11,0x00,0x11,0x00,0xFF,0xF0,0x11,0x00,0x11,0x00,0x11,0x00,0x1F,0x80,0x00,0x00,0x00,0x00,/*"中",0*/
"文",0x20,0x10,0x20,0x10,0x38,0x20,0x26,0x20,0xA1,0x40,0x60,0x80,0x21,0x40,0x26,0x20,0x38,0x20,0x20,0x10,0x20,0x10,0x00,0x00,/*"文",1*/
"字",0x30,0x80,0x20,0x80,0x24,0x80,0x24,0x90,0xA4,0x90,0x64,0xF0,0x25,0x80,0x26,0x80,0x24,0x80,0x20,0x80,0x30,0x80,0x00,0x00,/*"字",2*/
"体",0x08,0x00,0x3F,0xF0,0xC0,0x80,0x11,0x00,0x12,0x40,0x14,0x40,0xFF,0xF0,0x14,0x40,0x12,0x40,0x11,0x00,0x00,0x80,0x00,0x00,/*"体",3*/
"测",0x44,0x20,0x22,0x40,0x7F,0x90,0x40,0x20,0x5F,0xC0,0x40,0x20,0x7F,0x90,0x00,0x00,0x3F,0x80,0x00,0x10,0xFF,0xF0,0x00,0x00,/*"测",4*/
"试",0x88,0x00,0x4F,0xF0,0x00,0x20,0x00,0x00,0x24,0x20,0x27,0xE0,0x24,0x40,0x20,0x00,0xFF,0xC0,0x20,0x20,0xA0,0x70,0x00,0x00,/*"试",5*/
};
程序编写
1 |
|
主程序:
1 |
|
这里特别说明一下 LCD_ShowChineseString
函数中 s 指针偏移为3,是因为stm32cubeide默认编码是 utf-8,所以导致主程序中的中文会使用三个字节表示。
另外我们可以将包含中文字符的源文件格式修改为 GBK 格式,则这里 s偏移就需要修改为2。偏移为2是大多数教程例程的写法,因为他们使用的IDE编码就是GBK格式。这里为了统一、同时也为了兼容后续的中文字库程序,还是修改一下。
- 使用 vscode 打开该文件,并在vscode右小角单击 UTF-8,在命令栏中选择
通过编码重新保存
,并选择 GB 2312 格式,保存退出
- 右键包含中文的文件,单击
Properties
- 在弹出界面中,修改文件编码格式为 GBK。
- 记得将上面程序的偏移改为2。则可以下载使用了。
图片显示
取模
- 打开 Img2LCD 软件
- 打开要取模的图片:以例程的40x40企鹅图片为例
- 观察左下角的输入图像,如果显示无效的输入图像,那么请使用电脑自带的画图软件将图片转化为16色位的bmp格式图片。
- 打开图片后 设置如下
输出数据类型:C语言数组
扫描模式:水平
输出灰度:16位真彩色
尺寸请和实际尺寸一致,
此软件只能缩小图片不能放大图片!缩小是等比例缩小!
设置好后点击一下高位在前,其余不勾选
5. 然后点击保存,将生成的数组复制到到例程文件内
程序编写
1 |
|
主程序
1 |
|
显示flash中的图片
由于图片一般较大,我们一般会将图片存储在外置flash中。
取模
取模方式与上文基本一致,仅需要将输出类型改为 .bin
格式。
程序编写
该程序需要使用 usb u功能和fatf是文件系统。请参考usb U盘
和 fatfs文件系统移植
两个章节内容。这里只讲显示函数编写。
1 |
|
主函数
1 |
|
下载程序之后,将bin格式图片文件通过usb连接放到flash中。 名称要和程序中的一致 img_test.bin
。之后程序会查找该文件,并显示。
显示falsh中的中文字符
字库制作
- 打开点阵字库软件 ts3
- 选择字体。 字符集实测西欧语言和GB2312都可以。这里的字体大小设置于最终字库无关,字体大小由下面几个步骤决定。
- 设置宽、高(点阵大小)。字体大小设置请根据实际适应,保证字在方框中即可。一般16点阵字体大小12,24点阵字体大小18,32点阵字体大小24
- 横向、纵向偏移根据预览调整,保证字体居中方框即可
- 模式设置,设置
纵向取模方式二
(根据程序适配) - 点击创建保存,保存为 GBK16.DZK 。这里的命名适合下面的程序保持一致的,可以自定义,两者保持一致即可。
程序编写
该程序需要使用 usb u功能和fatf是文件系统。请参考usb U盘
和 fatfs文件系统移植
两个章节内容。这里只讲显示函数编写。
1 |
|
主程序调用:
1 |
|
这里说明以下,stm32cubeide默认编码是 utf-8,所以main.c 中文会无法显示,需要先将包含中文(注释不算)的程序源文件拜编码格式修改为 GBK 格式,才能下载使用。修改方式见上文 中文字符
章节
将上述生成的字库通过usb保存到flash中。Get_HzMat
函数中会查找该字库,请保持名称一致。
图片显示 DMA
1 |
|
这里唯一注意的点就是DMA传输的数据大小是16位的,也就是一次只能传出最多65535个数据。所以对于大的图片需要分次显示。
主函数:
1 |
|
DMA显示flash中的图片
取模
取模方式与上文基本一致,仅需要将输出类型改为 .bin
格式。
程序编写
程序结合 显示flash中的图片
和 图片显示 DMA
两个章节。
1 |
|
定义缓存数组pic_buffer
,存储读取的图片数据。DMA负责显示,检查DMA完成回调函数,判断下一次显示执行。
这里flash和lcd共用一个SPI接口,导致DMA显示时,无法读取falsh,如果时分开的接口,则可以实现DMA发送显示同时,读取下一次的图片数据,再判断DMA传输完成,这样可以加快显示速度。少了一个等待flash读取时间。
主程序:
1 |
|
CAN 通信
F103
官方HAL库中使用说明
#####如何使用这个驱动程序#####
通过实现 HAL_CAN_MspInit() 来初始化 CAN 低级资源:
使用 __HAL_RCC_CANx_CLK_ENABLE() 启用 CAN 接口时钟
配置 CAN 引脚
- 启用 CAN GPIO 的时钟
- 将 CAN 引脚配置为备用功能开漏
- 在使用中断的情况下(例如 HAL_CAN_ActivateNotification())
- 使用HAL_NVIC_SetPriority()配置 CAN 中断优先级
- 使用 HAL_NVIC_EnableIRQ() 启用 CAN IRQ 处理程序
- 在 CAN IRQ 处理程序中,调用 HAL_CAN_IRQHandler()
使用 HAL_CAN_Init() 函数初始化 CAN 外设。该函数使用 HAL_CAN_MspInit() 进行低级初始化。
使用以下配置函数配置接收过滤器:
- HAL_CAN_ConfigFilter()
使用 HAL_CAN_Start() 函数启动 CAN 模块。在这个级别,节点在总线上处于活动状态:它接收消息,并且可以发送消息。
要管理消息传输,可以使用以下 Tx 控制函数:
HAL_CAN_AddTxMessage() 请求传输新消息。
HAL_CAN_AbortTxRequest() 中止传输未决消息。
HAL_CAN_GetTxMailboxesFreeLevel() 获取空闲 Tx 邮箱的数量。
HAL_CAN_IsTxMessagePending() 检查消息是否在 Tx 邮箱中挂起。
HAL_CAN_GetTxTimestamp() 如果启用了时间触发通信模式,则获取发送的 Tx 消息的时间戳。当 CAN Rx FIFO 接收到消息时,可以使用 HAL_CAN_GetRxMessage() 函数检索它。函数 HAL_CAN_GetRxFifoFillLevel() 允许知道有多少 Rx 消息存储在 Rx Fifo 中。
调用 HAL_CAN_Stop() 函数停止 CAN 模块。
使用 HAL_CAN_DeInit() 函数实现去初始化。
轮询模式操作
接待:
- 使用 HAL_CAN_GetRxFifoFillLevel() 监控消息的接收,直到至少收到一条消息。
- 然后使用 HAL_CAN_GetRxMessage() 获取消息。
传播:
- 使用 HAL_CAN_GetTxMailboxesFreeLevel() 监控 Tx 邮箱的可用性,直到至少有一个 Tx 邮箱空闲。
- 然后使用HAL_CAN_AddTxMessage()。请求传输消息
中断模式操作
使用 HAL_CAN_ActivateNotification() 函数激活通知。然后,可以通过可用的用户回调控制该过程:HAL_CAN_xxxCallback(),同时使用 APIs HAL_CAN_GetRxMessage() 和 HAL_CAN_AddTxMessage()。
可以使用HAL_CAN_DeactivateNotification() 函数禁用通知
应特别注意 CAN_IT_RX_FIFO0_MSG_PENDING 和CAN_IT_RX_FIFO1_MSG_PENDING 通知。这些通知触发回调 HAL_CAN_RxFIFO0MsgPendingCallback() 和 HAL_CAN_RxFIFO1MsgPendingCallback()。用户有两种可能的选择 这里。
- 直接在回调中获取 Rx 消息,使用 HAL_CAN_GetRxMessage()。
- 或者在没有收到 Rx 消息的情况下停用回调中的通知。然后可以稍后使用 HAL_CAN_GetRxMessage() 获取 Rx 消息。读取 Rx 消息后,可以再次激活通知。
*** 睡眠模式 ***
CAN 外设可以使用 HAL_CAN_RequestSleep() 进入睡眠模式(低功耗)。一旦当前的 CAN 活动(CAN 帧的发送或接收)完成,就会进入睡眠模式。
可以激活通知,以便在进入睡眠模式时得到通知。
可以使用 HAL_CAN_IsSleepActive() 检查是否进入睡眠模式。
请注意,一旦提交睡眠模式请求(尚未进入睡眠模式),CAN 状态(可从 API HAL_CAN_GetState() 访问)为 HAL_CAN_STATE_SLEEP_PENDING,当睡眠模式有效时变为 HAL_CAN_STATE_SLEEP_ACTIVE。从睡眠模式唤醒可以通过两种方式触发:
- 使用 HAL_CAN_WakeUp()。 从该函数返回时,退出睡眠模式(如果返回状态为 HAL_OK)。
- 当 CAN 外设检测到 Rx CAN 帧的开始时,如果启用了自动唤醒模式。
使用
初始化使用CubeIDE直接配置,系统自动完成(CAN基础配置,FIFO配置等),不需要关心
重点关注以下函数:
1 |
|
LCD触摸
LVGl 移植
LL库
STM32LL库系列教程(一)—— LL库概览及资料
【stm32cubemx专题教程】ST全外设原理、配置、API使用详解
STM32 之十一 LL 库(low-layer drivers)详解 及 移植说明
标准库官方已经不更新了,虽然资料很多,所以不再使用。之后学习使用了HAL库,但最近做项目需要使用16和32KB的STM32F0芯片,使用HAL库新建个工程再加上串口,基本就是10KB+了,所以也是被迫重新选择了LL库。
下面是别人做的一个不同编程方式的效率对比:
原文链接:https://blog.csdn.net/super828/article/details/79078693
总的来说:代码效率与移植性成反比的规律是明显的。与HAL相比,LL的效率优势很明显,几乎和直接写寄存器的效率相差无几。而且目前STM32cubeIDE已经支持直接生成LL工程,对于追求效率的开发应用人员来说,非常值得推荐大家使用。
GPIO操作
配置操作基本和前面的 HAL 一样,只有一处不同,驱动库选择 LL 库,如图所示:
示例:
1 |
|
API详细使用请参考官方驱动描述手册:Description of STM32F4 HAL and low-layer drivers
USART
不论是重定义和自定义printf函数,若想打印float类型,都需要再IDE中单独开启,否则无法打印,且额外占用内存 18 KB 左右。
http://begild.top/article/a854db16.html
printf重定义
1 |
|
备注:
1、知识点:va_list
3、自己编写 printf 函数比重定义节省 0.46 KB,但RAM增加了。
其他发送方法
1、只发送字符串数据
1 |
|
备注:
不可使用DMA3,会出现 “VDD VALUE” redefined 错误。
只要配置了DMA,延时函数LL_mDelay()失效
总结
ADC
1 |
|