stm32学习笔记
文章目录
- stm32学习笔记
- 前言的前言
- 前言
- 笔记
- stm32 三种开发方式的区别
- 为什么stm32初始化外设都需要先打开时钟
- gpio八种模式
- stm32寄存器映射
- stm32中的位段映射
- stm32中的时钟系统
- stm32外设
- stm32的端口复用与重映射
- stm32中断
- stm32的usart使用
- stm32的外部中断 (exti)
- stm32库函数 exti_getflagstatus 和 exti_getitstatus 区别
- stm32的电源控制 (pwr)
- stm32中的备份寄存器 (bkp)
- stm32中的实时时钟 (rtc)
- stm32的低功耗模式
- stm32的模数转换(adc)
- stm32中的直接存储器存取 (dma)
- stm32中的集成电路总线 (i2c/iic)
- stm32中的串行外设接口 (spi)使用
- stm32中的控制器局域网 (bxcan)使用
前言的前言
文章的原标题是【stm32学习笔记】,但是在这个浮躁的时代,不当个标题狗是不会有人点进来的。而既然是发布出来了,那肯定是想要别人点个赞,点个收藏关注一下的,所以在发布的时候还是换了一个浮夸点的标题了。
前言
本文章主要记录本人在学习stm32过程中的笔记,也插入了不少的例程代码,方便到时候cv。绝大多数内容为本人手写,小部分来自stm32官方的中文参考手册以及网上其他文章;代码部分大多来自江科大和正点原子的例程,注释是我自己添加;配图来自江科大/正点原子/中文参考手册。
笔记内容都是平时自己一点点添加,不知不觉都已经这么长了。其实每一个标题其实都可以发一篇,但是这样搞太琐碎了,所以还是就这样吧。
喜欢的话,就点赞收藏关注一下~
本人技术有限,如有错误,欢迎在评论区或者私信指点。
笔记
本笔记内容以 stm32f103xx 型号为研究对象。
stm32 三种开发方式的区别
- 寄存器模式:最底层的开发,运行速度最快。实际上也是使用了固件库,但是不是使用固件库的函数,而是使用了固件库的定义,包括宏定义,结构体定义。和51的开发差不多,但因为32的寄存器太多,实际开发手动配置大量寄存器极其耗费时间,同时在没有注释的情况下可读性差,所以较少使用。
- 标准库模式:基于寄存器进行了函数的封装,而由于函数封装以及内部大量的检查参数有效性的代码,运行速度相对于寄存器模式较慢。封装之后可以根据函数名字就能明白代码作用,容易记忆,使用方便,所以较多人使用。
- hal库模式:全称是hardware abstraction layer(抽象印象层),相比于标准库更加深入的封装,有句柄、回调函数等概念(ps:有点类似windows开发),因此相对于标准库模式有更好的可移植性(可在不同芯片的移植),但代价就是更多的性能损失。
说明:运行速度,性能损失的问题,都只是相对问题,实际上大多数情况下都可以忽略。
为什么stm32初始化外设都需要先打开时钟
- 每个外设都有独立时钟,如果不打开时钟外设就不能用,原因就是为了低功耗节省用电,不用的外设可以不打开时钟
开启外设时钟的方法:
/*
ahb外设总线:
dma1,dma2,sram,flitf,crc,fsmc,sdio
*/
rcc_ahbperiphclockcmd(rcc_ahbperiph_crc,enable);
rcc_ahbperiphclockcmd(rcc_ahbperiph_crc,disable);
/*
apb1外设总线:
tim2,tim3,tim4,tim5,tim6,tim7,tim12,tim13,tim14,wwdg
spi2,spi3,usart2,usart3,uart4,uart5,i2c1,i2c2,usb,can1,can2,bkp,pwr,dac,cec,
*/
rcc_apb1periphclockcmd(rcc_apb1periph_spi2,enable);
rcc_apb1periphclockcmd(rcc_apb1periph_spi2,disable);
/*
apb2外设总线:
afio,gpioa,gpiob,gpioc,gpiod,gpioe,gpiof,gpiog,adc1,adc2
tim1,spi1,tim8,usart1,adc3,tim15,tim16,tim17,tim9,tim10,tim11
*/
rcc_apb2periphclockcmd(rcc_apb2periph_gpioa, enable);
rcc_apb2periphclockcmd(rcc_apb2periph_gpioa, disable);
gpio八种模式
模式 | 介绍 | |
---|---|---|
浮空输入 | gpio_mode_in_floating | 若引脚悬空,则电平不确定 |
上拉输入 | gpio_mode_ipu | 内部连接上拉电阻,悬空时默认高电平 |
下拉输入 | gpio_mode_ipd | 内部连接下拉电阻,悬空时默认低电平 |
模拟输入 | gpio_mode_ain | gpio无效,引脚直接接入内部adc |
开漏输出 | gpio_mode_out_od | 高电平为高阻态,低电平接vss(负极) |
推挽输出 | gpio_mode_out_pp | 高电平接vdd,低电平接vss |
复用开漏输出 | gpio_mode_af_od | 由片上外设控制,高电平为高阻态,低电平接vss |
复用推挽输出 | gpio_mode_af_pp | 由片上外设控制,高电平接vdd,低电平接vss |
更加详细请看:gpio口8种模式详解
stm32寄存器映射
以最简单的gpio讲,将 gpioa 相关的固件库代码拿出来变很容易明白。
#define periph_base ((uint32_t)0x40000000) //外设基地址
#define apb2periph_base (periph_base + 0x10000) //apb2总线基地址
#define gpioa_base (apb2periph_base + 0x0800) //gpioa 基地址
typedef struct
{
__io uint32_t crl;
__io uint32_t crh;
__io uint32_t idr;
__io uint32_t odr;
__io uint32_t bsrr;
__io uint32_t brr;
__io uint32_t lckr;
} gpio_typedef;
#define gpioa ((gpio_typedef *) gpioa_base) //gpioa结构
很明显可以看出来,固件库代码的条理非常清晰,而且非常巧妙。除了第一个外设基地址是固定值,其他的基地址都是通过 上一级基地址+偏移 计算出来的,最后gpioa是一个 指定地址强制转换结构。
这样我们如果想要操作寄存器,则可以用
gpioa->crl&=0xff0fffff; //将寄存器 20~23位 置0
gpioa->crl|=0x00300000; //设置寄存器 20~23位,实际作用是设置pa5为推挽输出
gpioa->odr|=1<<5; //pa5 输出高电平
另外可以注意到,所有地址都是使用了#define
定义常量值,这是因为编译器在进行项目编译的时候,对于常量间的计算,是能直接优化成常量值。如:
gpioa->crl&=0xff0fffff;
//进行预编译处理之后为:
((gpio_typedef *) ((((uint32_t)0x40000000) + 0x10000) + 0x0800))&=0xff0fffff;
//然后优化为:
((gpio_typedef *) ((uint32_t)0x40010800) &=0xff0fffff;
stm32中的位段映射
cortex?-m3存储器映像包括两个位段 (bit-band) 区。这两个位段区将别名存储器区中的每个字映射到位段存储器区的一个位,在别名存储区写入一个字具有对位段区的目标位执行读-改-写
操作的相同效果。
在stm32f10xxx里,外设寄存器和sram都被映射到一个位段区里,这允许执行单一的位段的写和读操作。
下面的映射公式给出了别名区中的每个字是如何对应位带区的相应位的:
例子: 下面的例子说明如何映射别名区中sram地址为 0x20000300
的字节中的 位2:
- 本节内容摘抄至
stm32f10xxx参考手册(中文).pdf
stm32中的时钟系统
stm32 有5个时钟源:hsi、 hse、lsi、lse、pll
中文名称 | 解释 | |
---|---|---|
hsi | 高速内部时钟 | rc振荡器,频率为8mhz,精度不高。 |
hse | 高速外部时钟 | 可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4mhz~16mhz。 |
lsi | 低速内部时钟 | rc振荡器,频率为40khz,提供低功耗时钟。一般用于看门狗(wdg) 担当一个低功耗时钟源的角色,它可以在停机和待机模式下保持运行,为独立看门狗和 自动唤醒单元提供时钟。 |
lse | 低速外部时钟 | 接频率为32.768khz的石英晶体。一般用于实时时钟(rtc) |
pll | 锁相环倍频输出 | 本质上与其他四个时钟源不一样,这个时钟源是将 时钟输入源 进行 倍频 再输出 时钟输入源可选择为 hsi / 2 、hse 或 hse / 2 。倍频可选择为2~16倍,但是其输出频率最大不得超过72mhz。 倍频器的原理:https://www.bilibili.com/video/bv1mq4y1g77m |
时钟安全系统(css)
stm32中还有一个时钟安全系统(css),在出现意外情况下还挺有用的。不过既然说是意外,就说明出现的概率并不大,因此这个功能没有什么存在感。
stm32外设
从图中就可以看出,apb1、apb2都是ahb系统总线进行桥接出来的。另外apb1最高只有36mhz,apb2最高可以达到72mhz。
stm32的各种外设:
- io口 (gpio)
- 定时器 (tim)
- 数模转换器 (dac)
- 模数转换器 (adc)
- 串口 (uart)
- 串行外设接口 (spi)
- 集成电路总线 (i2c/iic)
- 集成电路内置音频总线 (iis/i2s)
- 外部中断/事件控制器 (exti)
- 通用和复用功能io (afio)
- 独立看门狗 (iwdg)
- 窗口看门狗 (wwdg)
- 备份寄存器 (bkp)
- 实时时钟 (rtc)
- usb全速设备接口 (usb)
- 控制器局域网 (bxcan)
内核外设:
- 嵌套中断向量控制器 (nvic)
stm32的端口复用与重映射
stm32有很多的io口,同时有很多的外设。这些io口默认是用来做普通的输出输入引脚,而配置为外设需要用到io口,就叫io口的复用。如:
管脚名称 | 主功能 (复位后) | 默认复用功能 | 重定义功能 |
---|---|---|---|
pa9 | pa9 | usart1_tx | 无 |
pa10 | pa10 | usart1_rx | 无 |
/*
以下代码则是配置pa9、pa10为复用。
其实pa10作为输入引脚,并不区分复用不复用的,因为输出只能有一个外设控制,但是输入可以多个外设读取,不冲突。
*/
//需要使能gpio和复用外设的时钟,使用默认复用功能时,afio时钟不需要使能
rcc_apb2periphclockcmd(rcc_apb2periph_usart1, enable);
rcc_apb2periphclockcmd(rcc_apb2periph_gpioa, enable);
//初始化tx引脚 pa9 为复用推挽输出
gpio_inittypedef gpio_initstructure;
gpio_initstructure.gpio_mode = gpio_mode_af_pp; //复用推挽输出
gpio_initstructure.gpio_pin = gpio_pin_9;
gpio_initstructure.gpio_speed = gpio_speed_50mhz;
gpio_init(gpioa, &gpio_initstructure);
//初始化rx引脚 pa10 为上拉输入
gpio_initstructure.gpio_mode = gpio_mode_ipu;
gpio_initstructure.gpio_pin = gpio_pin_10;
gpio_initstructure.gpio_speed = gpio_speed_50mhz;
gpio_init(gpioa, &gpio_initstructure);
每个内置外设都有若干个输入输出引脚,一般这些引脚的输出端口都是固定不变的。但在实际使用中,为了让设计工程师可以更好地安排引脚的走向和功能,在stm32中引入了外设引脚重映射的概念。即一个外设的引脚除了具有默认的端口外,还可以通过设置重映射寄存器的方式,把这个外设的引脚映射到其它的端口。
管脚名称 | 主功能 (复位后) | 默认复用功能 | 重定义功能 |
---|---|---|---|
pb6 | pb6 | 1i2c1_scl / tim4_ch1 | usart1_tx |
pb7 | pb7 | i2c1_sda / fsmc_nadv / tim4_ch2 | usart1_rx |
如 外设的 usart1_tx
引脚除了pa9
外,还可以使用pb6
。
//使能重映射之后的gpio时钟
rcc_apb2periphclockcmd(rcc_apb2periph_gpiob, enable);
//使能复用外设的时钟
rcc_apb2periphclockcmd(rcc_apb2periph_usart1, enable);
//重映射需要使能afio时钟,因为下一行代码是配置afio_mapr寄存器
rcc_apb2periphclockcmd(rcc_apb2periph_afio, enable);
//实际上是对afio进行操作:重映射引脚
gpio_pinremapconfig(gpio_remap_usart1, enable);
//初始化pb6与pb7引脚,略
//...
部分重映射&完全重映射
部分重映射:功能外设的部分引脚重新映射,还有一部分引脚是原来的默认引脚
完全重映射:功能外设的所有引脚都重新映射
何时需要使能afio时钟?
根据手册说明:对寄存器afio_evcr(事件控制寄存器)、afio_mapr(复用重映射和调试i/o配置寄存器)和afio_exticrx(外部中断配置寄存器) 进行读写操作前,应当首先打开afio 的时钟。
stm32中断
stm32f103xx 中有60个可编程外设中断。配置中断的代码如下:
抢占优先级:优先级高的能打断优先级低
响应优先级:当抢占优先级相同时,响应优先级高的先执行
总结:抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队
#define nvic_prioritygroup_0 ((uint32_t)0x700) // 0位抢先优先级、4位响应优先级
#define nvic_prioritygroup_1 ((uint32_t)0x600) // 1位抢先优先级、3位响应优先级
#define nvic_prioritygroup_2 ((uint32_t)0x500) // 2位抢先优先级、2位响应优先级
#define nvic_prioritygroup_3 ((uint32_t)0x400) // 3位抢先优先级、1位响应优先级
#define nvic_prioritygroup_4 ((uint32_t)0x300) // 4位抢先优先级、0位响应优先级
nvic_prioritygroupconfig (nvic_prioritygroup_2); //设置优先级分配配置
nvic_inittypedef nvic_initstructure;
nvic_initstructure.nvic_irqchannel = usart1_irqn; //设置中断通道类型
nvic_initstructure.nvic_irqchannelcmd = enable; //设置中断使能
/*优先级的值越小,优先级越高(越先执行)*/
nvic_initstructure.nvic_irqchannelpreemptionpriority = 1; //设置抢占优先级
nvic_initstructure.nvic_irqchannelsubpriority = 1; //设置响应优先级
nvic_init(&nvic_initstructure); //初始化中断通道
stm32的usart使用
void serial_init(void)
{
//使用之前需要先启用外设 usart1,gpioa
rcc_apb2periphclockcmd(rcc_apb2periph_usart1, enable);
rcc_apb2periphclockcmd(rcc_apb2periph_gpioa, enable);
//初始化tx引脚 pa9 为复用推挽输出
gpio_inittypedef gpio_initstructure;
gpio_initstructure.gpio_mode = gpio_mode_af_pp;
gpio_initstructure.gpio_pin = gpio_pin_9;
gpio_initstructure.gpio_speed = gpio_speed_50mhz;
gpio_init(gpioa, &gpio_initstructure);
//初始化rx引脚 pa10 为上拉输入
gpio_initstructure.gpio_mode = gpio_mode_ipu;
gpio_initstructure.gpio_pin = gpio_pin_10;
gpio_initstructure.gpio_speed = gpio_speed_50mhz;
gpio_init(gpioa, &gpio_initstructure);
//初始化 usart1 为波特率9600,无硬流控,需要收发,无校验,1位停止位
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;
usart_initstructure.usart_wordlength = usart_wordlength_8b;
usart_init(usart1, &usart_initstructure);
//开启rxne标志位到nvic的输出
usart_itconfig(usart1, usart_it_rxne, enable);
//设置优先级分配配置
nvic_prioritygroupconfig(nvic_prioritygroup_2);
//配置 usart1 的中断
nvic_inittypedef nvic_initstructure;
nvic_initstructure.nvic_irqchannel = usart1_irqn;
nvic_initstructure.nvic_irqchannelcmd = enable;
nvic_initstructure.nvic_irqchannelpreemptionpriority = 1;
nvic_initstructure.nvic_irqchannelsubpriority = 1;
nvic_init(&nvic_initstructure);
//最后使能 usart1
usart_cmd(usart1, enable);
}
void serial_sendbyte(uint8_t byte)
{
usart_senddata(usart1, byte); //填充数据至 usart1的dr寄存器
//usart_flag_txe: 发送寄存器为空标志位。对usart_dr的写操作时,将该位清零。
while (usart_getflagstatus(usart1, usart_flag_txe) == reset); //等待发送完成
}
//usart1 中断函数
void usart1_irqhandler(void)
{
if (usart_getitstatus(usart1, usart_it_rxne) == set)
{
uint8_t serial_rxdata = usart_receivedata(usart1); //读取 usart1 收到的字节
/*
usart_clearitpendingbit(usart1, usart_it_rxne);
这里可以省略手动清除标志位,因为对usart_dr的读操作可以将该位清零。
*/
}
}
stm32的外部中断 (exti)
/*
配置外部中断的示例代码
*/
void exti(void)
{
//使能gpioa时钟
rcc_apb2periphclockcmd(rcc_apb2periph_gpioa, enable);
//因为使用到了afio的中断引脚选择功能,所以要使能afio的时钟
rcc_apb2periphclockcmd(rcc_apb2periph_afio, enable);
gpio_inittypedef gpio_initstructure;
gpio_initstructure.gpio_mode = gpio_mode_ipu;
gpio_initstructure.gpio_pin = gpio_pin_14;
gpio_initstructure.gpio_speed = gpio_speed_50mhz;
gpio_init(gpioa, &gpio_initstructure);
//实际上是对afio进行操作:将pa14信号输出至exti的14号线
gpio_extilineconfig(gpio_portsourcegpioa, gpio_pinsource14);
//初始化exti
exti_inittypedef exti_initstructure;
exti_initstructure.exti_line = exti_line14;
exti_initstructure.exti_linecmd = enable;
exti_initstructure.exti_mode = exti_mode_interrupt; //使用中断
exti_initstructure.exti_trigger = exti_trigger_falling; //下降沿触发
exti_init(&exti_initstructure);
//设置优先级分配配置
nvic_prioritygroupconfig(nvic_prioritygroup_2);
//配置外部中断
nvic_inittypedef nvic_initstructure;
nvic_initstructure.nvic_irqchannel = exti15_10_irqn;
nvic_initstructure.nvic_irqchannelcmd = enable;
nvic_initstructure.nvic_irqchannelpreemptionpriority = 1;
nvic_initstructure.nvic_irqchannelsubpriority = 1;
nvic_init(&nvic_initstructure);
}
//中断函数
void exti15_10_irqhandler(void)
{
if (exti_getitstatus(exti_line14) == set)
{
//清除中断标志位
exti_clearitpendingbit(exti_line14);
}
}
stm32库函数 exti_getflagstatus 和 exti_getitstatus 区别
flagstatus exti_getflagstatus(uint32_t exti_line)
{
flagstatus bitstatus = reset;
/* check the parameters */
assert_param(is_get_exti_line(exti_line));
if ((exti->pr & exti_line) != (uint32_t)reset)
{
bitstatus = set;
}
else
{
bitstatus = reset;
}
return bitstatus;
}
itstatus exti_getitstatus(uint32_t exti_line)
{
itstatus bitstatus = reset;
uint32_t enablestatus = 0;
/* check the parameters */
assert_param(is_get_exti_line(exti_line));
enablestatus = exti->imr & exti_line;
if (((exti->pr & exti_line) != (uint32_t)reset) && (enablestatus != (uint32_t)reset))
{
bitstatus = set;
}
else
{
bitstatus = reset;
}
return bitstatus;
}
可以很容易看出来,代码上的区别在:
exti_getflagstatus 部分:
if ((exti->pr & exti_line) != (uint32_t)reset)
exti_getitstatus 部分:
enablestatus = exti->imr & exti_line;
if (((exti->pr & exti_line) != (uint32_t)reset) && (enablestatus != (uint32_t)reset))
即 exti_getitstatus 的判断多了一个条件。
由手册可以知道:
因此,exti_getflagstatus
只是纯粹读取中断标志位的状态,但是实际上这并不准确,因为设置 exti_imr 寄存器可以对该中断进行屏蔽;而 exti_getitstatus
除了读取中断标志位,还查看 exti_imr 寄存器是否对该中断进行屏蔽。
另外,exti_clearflag
和 exti_clearitpendingbit
则是什么区别都没有,内部代码完全一样。
stm32的电源控制 (pwr)
stm32的工作电压(vdd)为2.0~3.6v。通过内置的电压调节器提供所需的1.8v电源。 当主电源vdd掉电后,通过vbat脚为实时时钟(rtc)和备份寄存器提供电源。实际上,vbat脚还可以为 lse振荡器 和 pc13~pc15 端口供电,可以保证当主电源被切断时rtc能继续工作。但当使用vbat供电时,pc13~pc15无法用作gpio。
管脚名称 | 主功能 (复位后默认) | 复用功能 | 功能 |
---|---|---|---|
pc13 | pc13 | tamper / rtc | 用于侵入检测,rtc校准时钟、rtc闹钟或秒输出 |
pc14 | pc14 | osc32_in | lse引脚 |
pc15 | pc15 | osc32_out | lse引脚 |
从图中可以看出来,除了上面说到的之外,rcc_bdcr 寄存器也在后备供电区域内。但实际上,rcc_bdcr 寄存器只有 lseon (外部低速振荡器使能)
、lsebyp (外部低速时钟振荡器旁路)
、rtcsel (rtc时钟源选择)
和 rtcen (rtc时钟使能)
位处于备份域。另外的 lserdy (外部低速lse就绪)
与 bdrst (备份域软件复位)
不处于备份域,因为没有必要。
stm32中的备份寄存器 (bkp)
备份寄存器拥有以下特性
- 当vdd电源被切断,他们仍然由vbat维持供电。
- 20字节数据后备寄存器(中容量和小容量产品),或84字节(42*16 bit)数据后备寄存器(大容量和互联型 产品)
- 当系统在待机模式下被唤醒,或系统复位或 电源复位时,他们也不会被复位。
- bkp寄存器是16位的可寻址寄存器,可以用半字(16位)或字(32位)的方式操作这些外设寄存器。
备份寄存器的复位
- 软件复位,备份区域复位可由设置备份域控制寄存器 (rcc_bdcr)中的 bdrst位产生
- 在vdd和vbat两者都掉电的情况下,vdd或vbat上电将引发备份区域复位。
后备区域的保护
在复位之后,对 后备区域(备份寄存器和rtc)
的访问将被禁止,后备区域被保护以防止可能存在的意外的写操作。
需要执行以下操作可以使能对后备区域的访问。
- 通过设置寄存器 rcc_apb1enr 的 pwren 和 bkpen 位来打开电源和后备接口的时钟
- 说人话就是使能
电源控制 (pwr)
与备份寄存器 (bkp)
的时钟
- 说人话就是使能
- 电源控制寄存器(pwr_cr)的dbp位来使能对后备寄存器和rtc的访问
/*
bkp寄存器基础操作示例
*/
rcc_apb1periphclockcmd(rcc_apb1periph_pwr | rcc_apb1periph_bkp, enable); //使能pwr和bkp外设时钟
bkp_readbackupregister(bkp_dr1) //读取 bkp_dr1 寄存器,启用时钟后就可以读取了
bkp_deinit() //对备份寄存器进行软件复位
pwr_backupaccesscmd(enable); //取消后备区域的写保护,但如果rtc的时钟是hse/128,无法进行写保护。
bkp_writebackupregister(bkp_dr1, 0x5050); //向 bkp_dr1 寄存器写 0x5050,写之前要取消写保护才可以
stm32中的实时时钟 (rtc)
rtc的本质与定时器类似,就是一个计数器,每秒加一让其可以实现更新时间。
- rtc的预分配系数最高为2的20次方
- rtc的计数器是32位的
- rtc的时钟源可以选择以下三种
- rcc_rtcclksource_lse:低速外部时钟
- rcc_rtcclksource_lsi:低速内部时钟 (通常用这个作为时钟源,32.768 khz 进行 32768 分配可以得到 1hz 的时钟信号)
- rcc_rtcclksource_hse_div128:高速外部时钟的128分频
- rtc的3个可屏蔽中断
- 闹钟中断:用来产生一个软件可编程的闹钟中断
- 秒中断:用来产生一个可编程的周期性中断信号(最长可达1秒)
- 溢出中断:指示内部可编程计数器溢出并回转为0的状态
rtc的时钟源的配置是设置 备份域控制寄存器 (rcc_bdcr) 里的 rtcsel[1:0] 位。因此,除非备份域复位,不然此选择不能被改变。
读rtc寄存器
rtc核完全独立于rtc apb1接口。软件通过apb1接口访问rtc的预分频值、计数器值和闹钟值。但是,相关的可读寄存器只在与 rtc apb1时钟进行重新同步的rtc时钟的上升沿被更新。(rtc标志也是如此的)
这意味着,如果apb1接口曾经被关闭,而读操作又是在刚刚重新开启apb1之后,则在第一次的内部寄存器更新之前,从apb1上读出的rtc寄存器数值可能被破坏了(通常读到0)。
下述几种情况下能够发生这种情形:
- 发生系统复位或电源复位
- 系统刚从待机模式唤醒
- 系统刚从停机模式唤醒
所有以上情况中,apb1接口被禁止时(复位、无时钟或断电),rtc核仍保持运行状态。
因此,若在读取rtc寄存器时,rtc的apb1接口曾经处于禁止状态,则软件首先必须等待 rtc_crl寄存器中的rsf位(寄存器同步标志)被硬件置’1’。
写rtc寄存器
必须设置rtc_crl寄存器中的cnf位,使rtc进入配置模式后,才能写入 rtc_prl(预分频装载寄存器)
、 rtc_cnt(计数器寄存器)
、 rtc_alr(闹钟寄存器)
。
另外,对rtc任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询 rtc_cr寄存器中的rtoff状态位,判断rtc寄存器是否处于更新中。仅当rtoff状态位是’1’ 时,才可以写入rtc寄存器。
配置过程:
- 查询rtoff位,直到rtoff的值变为’1’
- 置cnf值为1,进入配置模式
- 对一个或多个rtc寄存器进行写操作
- 清除cnf标志位,退出配置模式
- 查询rtoff,直至rtoff位变为’1’以确认写操作已经完成。
仅当cnf标志位被清除时,写操作才能进行,这个过程至少需要3个rtcclk周期。
/*
rtc初始化与中断
*/
u8 rtc_init(void)
{
u8 temp = 0;
rcc_apb1periphclockcmd(rcc_apb1periph_pwr | rcc_apb1periph_bkp, enable); // 使能pwr和bkp外设时钟
pwr_backupaccesscmd(enable); // 取消后备区域(rtc和后备寄存器)的写保护
// 判断
if (bkp_readbackupregister(bkp_dr1) != 0x5050)
{
bkp_deinit(); //对备份寄存器进行软件复位
rcc_lseconfig(rcc_lse_on); //使能 外设低速晶振
//检查指定的rcc标志位设置与否,等待低速晶振就绪
while (rcc_getflagstatus(rcc_flag_lserdy) == reset && temp < 250)
{
temp++;
delay_ms(10);
}
if (temp >= 250)
return 1; //超时说明初始化时钟失败,晶振有问题
rcc_rtcclkconfig(rcc_rtcclksource_lse); //设置 lse 作为 rtc时钟源
rcc_rtcclkcmd(enable); //使能rtc时钟,要先设置时钟源
rtc_waitforsynchro(); // 等待rtc寄存器同步
rtc_waitforlasttask(); // 等待最近一次对rtc寄存器的写操作完成
rtc_itconfig(rtc_it_sec, enable); // 使能rtcf的秒中断
rtc_waitforlasttask(); // 等待最近一次对rtc寄存器的写操作完成
rtc_setprescaler(32767); // 设置rtc预分频的值
rtc_waitforlasttask(); // 等待最近一次对rtc寄存器的写操作完成
rtc_setcounter(123456); // 设置计数值(时间戳)
/*
实际上用不上,因为库函数封装中已经包含,不需要自己手动额外写
rtc_enterconfigmode(); // 允许配置
rtc_exitconfigmode(); // 退出配置模式
*/
bkp_writebackupregister(bkp_dr1, 0x5050); // 向指定的后备寄存器中写入用户程序数据
}
else // 系统继续计时
{
rtc_waitforsynchro(); // 等待rtc寄存器同步
rtc_itconfig(rtc_it_sec, enable); // 使能rtc秒中断
rtc_waitforlasttask(); // 等待最近一次对rtc寄存器的写操作完成
}
//初始化中断通道
nvic_inittypedef nvic_initstructure;
nvic_initstructure.nvic_irqchannel = rtc_irqn; // rtc全局中断
nvic_initstructure.nvic_irqchannelpreemptionpriority = 0; // 先占优先级1位,从优先级3位
nvic_initstructure.nvic_irqchannelsubpriority = 0; // 先占优先级0位,从优先级4位
nvic_initstructure.nvic_irqchannelcmd = enable; // 使能该通道中断
nvic_init(&nvic_initstructure);
return 0;
}
void rtc_irqhandler(void)
{
if (rtc_getitstatus(rtc_it_sec) != reset) // 秒钟中断
{
rtc_waitforsynchro(); // 等待rtc寄存器同步,读取rtc寄存器前必须做
rtc_getcounter(); // 获取当前计数值(时间戳)
}
if (rtc_getitstatus(rtc_it_alr) != reset) // 闹钟中断
{
rtc_clearitpendingbit(rtc_it_alr); // 清闹钟中断
}
rtc_clearitpendingbit(rtc_it_sec | rtc_it_ow); // 清秒中断与溢出中断
rtc_waitforlasttask();
}
stm32的低功耗模式
stm32f10xxx有三种低功耗模式:
除了进行低功耗模式外,还可以在正常运行时使用下面方法降低功耗:
- 降低系统时钟
- 关闭apb和ahb总线上未被使用的外设时钟
睡眠模式:
在睡眠模式下,仅停止cpu运作,对于其他外设,将保持原本进入睡眠模式的状态。
有两种选项可用于选择睡眠模式进入机制
- sleep-now:如果sleeponexit位被清除,当wri或wfe被执行时,微控制器立即进入睡眠模式。
- sleep-on-exit:如果sleeponexit位被置位,系统从最低优先级的中断处理程序中退出时,微控制器就立即进入睡眠模式
停止模式:
在停止模式下,除了sram(内存)和寄存器内容被保留下来外,其他时钟将会被停止,所有的i/o引脚都保持它们在运行模式时的状态。另外,
进入停止模式需要等待闪存编程与apb访问完成,不然会等待完成再进入。
当一个中断或唤醒事件导致退出停止模式时,hsi rc振荡器将被选为系统时钟。
为了进入停止模式,所有的外部中断的请求位(挂起寄存器(exti_pr))和rtc的闹钟标志都必须被清除,否则停止模式的进入流程将会被跳过,程序继续运行。
进入停止模式可以配置以下外设正常运行:
-
独立看门狗(iwdg):可通过写入看门狗的键寄存器或硬件选择来启动iwdg。一旦启动了独立看门狗,除了系统复位,它不能再被停止
-
实时时钟(rtc):通过备份域控制寄存器 (rcc_bdcr)的rtcen位来设置
-
内部rc振荡器(lsi rc):通过控制/状态寄存器 (rcc_csr)的lsion位来设置
-
外部32.768khz振荡器(lse):通过备份域控制寄存器 (rcc_bdcr)的lseon位设置
-
adc与dac:如果在进入该模式前adc和dac没有被关闭,那么这些外设仍然消耗电流。通过设置寄存器adc_cr2 的 adon 位和寄存器 dac_cr 的 enx 位为0可关闭这2个外设
-
电压调节器:可以通过配置电源控制寄存器(pwr_cr)的lpds位使其运行在正常或低功耗模式。
-
若配置电压调节器为低功耗模式,当系统从停止模式退出时,将会有一段额外的启动延时(hsi rc唤醒时间 + 电压调节器从低功耗唤醒的时间)。
-
如果在停止模式期间保持内部调节器开启,则退出启动时间会缩短,但相应的功耗会增加。
-
待机模式:
待机模式可实现系统的最低功耗,待机模式下只有备份寄存器和待机电路维持供电。从待机唤醒后,差不多和复位一次差不多,除了电源控制/状 态寄存器(pwr_csr),所有寄存器被复位。sram和寄存器内容全部丢失。
进入待机模式可以配置正常运行的外设只有停机模式的前四项。
在待机模式下,所有的i/o引脚处于高阻态,除了以下的引脚: 复位引脚(始终有效)、当被设置为防侵入或校准输出时的tamper引脚、被使能的唤醒引脚
低功耗模式下的自动唤醒(awu) :
利用rtc可以实现定时唤醒低功耗模式,实际上是使用了rtc的闹钟中断。
若要实现低功耗模式下的自动唤醒,rtc的时钟源只能选择:低功耗32.768khz外部晶振(lse) 或者 低功耗内部rc振荡器(lsi rc)。
为了用rtc闹钟事件将系统从停止模式下唤醒,必须进行如下操作:
- 配置外部中断线17为上升沿触发 (若要从待机模式唤醒则不必配置)
- 配置rtc使其可产生rtc闹钟事件
/*
三种模式的进入代码示例
*/
/*进入睡眠模式*/
/*
wfi与wfe属于arm核心指令,库函数中是汇编指令。
sleeponexit与_sleeponexit位属于arm架构的寄存器,在stm32手册中没有讲到寄存器地址,但是固件库也定义了相关的内容。
进入睡眠模式库函数没有封装,因此只能自己动手丰衣足食。
*/
//理论上sleepdeep位应该是不需要手动清除的,它默认为0,但是为了防止意外情况,就多写一行代码。
scb->scr &= (uint32_t)~((uint32_t)scb_scr_sleepdeep); //清除深睡眠(sleepdeep)位
//根据需要选择是否允许在中断过程中进入睡眠
scb->scr &= (uint32_t)~((uint32_t)scb_scr_sleeponexit); //清除scb_scr_sleeponexit位,sleep-now
//scb->scr |= scb_scr_sleeponexit; //设置scb_scr_sleeponexit位,sleep-on-exit
__wfi(); //进入等待中断的睡眠。与下面一行二选一即可
__wfe(); //进入等待事件的睡眠。
/*进入停机模式*/
rcc_apb1periphclockcmd(rcc_apb1periph_pwr, enable); //使能pwr外设时钟
pwr_enterstopmode(pwr_regulator_on, pwr_stopentry_wfi); //电压调节器开,等待中断模式
/*进入待机模式*/
rcc_apb1periphclockcmd(rcc_apb1periph_pwr, enable); //使能pwr外设时钟
pwr_wakeuppincmd(enable); //使能pa0引脚的唤醒管脚功能,如果不需要使用wkup引脚上升沿唤醒待机可以注释
pwr_enterstandbymode(); //进入待命(standby)模式
stm32的模数转换(adc)
规则组:用于常规使用
注入组:用于突发情况使用adc功能
输入通道:
因为stm32有双adc模式(两个adc配合工作),因此adc1和adc2的通道对应的io基本一样,除了adc1多出来的温度传感器与内部参考电压通道。
通道 | adc1 | adc2 | adc3 |
---|---|---|---|
通道0 | pa0 | pa0 | pa0 |
通道1 | pa1 | pa1 | pa1 |
通道2 | pa2 | pa2 | pa2 |
通道3 | pa3 | pa3 | pa3 |
通道4 | pa4 | pa4 | pf6 |
通道5 | pa5 | pa5 | pf7 |
通道6 | pa6 | pa6 | pf8 |
通道7 | pa7 | pa7 | pf9 |
通道8 | pb0 | pb0 | pf10 |
通道9 | pb1 | pb1 | |
通道10 | pc0 | pc0 | pc0 |
通道11 | pc1 | pc1 | pc1 |
通道12 | pc2 | pc2 | pc2 |
通道13 | pc3 | pc3 | pc3 |
通道14 | pc4 | pc4 | |
通道15 | pc5 | pc5 | |
通道16 | 温度传感器 | ||
通道17 | 内部参考电压 |
adc配置:
扫描模式
:当开始转换后,会根据adc通道数量(adc_inittypedef.adc_nbrofchannel) 按顺序进行n次转换,全部转换完成后设置 eoc(规则组转换结束) 标志位
非扫描模式
:当开始转换后,仅会对规则组位置一的通道进行1次转换,转换完成设置 eoc 标志位
单次转换
:在开始转换后,仅仅对规则组整组进行一次转换
连续转换
:在开始转换后,会循环对规则组整组进行转换
间断模式
:在开始转换后,进行 n 次转换后停下,并记录当前位置,当下次开始转换时按顺序下去。
总结一下:
校准:
adc有一个内置的校准模式,能大幅减少因内部电容器组的变化而造成的准精度误差。因此建议每次上电后都执行一次校准。
在 stm32f10xxx参考手册(2009中文版本) 中adc章节有这样一句话:
事实上,是st公司的描写错误,而在官网中找到的 2021 版本中已经被更正为
void ad_init(void)
{
//使能时钟
rcc_apb2periphclockcmd(rcc_apb2periph_adc1, enable);
rcc_apb2periphclockcmd(rcc_apb2periph_gpioa, enable);
//配置adc的时钟周期,rcc_pclk2_div6 为高速apb2时钟(pclk2)的6分频
rcc_adcclkconfig(rcc_pclk2_div6);
//配置pa0为输入口,模式为模拟输入(gpio_mode_ain),该模式是adc专用
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);
//配置规则组,将通道0放在第一个位置,采样时间为55.5个周期(adc_sampletime_55cycles5)
adc_regularchannelconfig(adc1, adc_channel_0, 1, adc_sampletime_55cycles5);
//初始化adc1
adc_inittypedef adc_initstructure;
adc_initstructure.adc_mode = adc_mode_independent; //工作在独立模式
adc_initstructure.adc_dataalign = adc_dataalign_right; //数据右对齐
adc_initstructure.adc_externaltrigconv = adc_externaltrigconv_none; //外部触发源选择不使用外部触发
adc_initstructure.adc_continuousconvmode = disable; //是否启用连续模式
adc_initstructure.adc_scanconvmode = disable; //是否启用扫描模式
adc_initstructure.adc_nbrofchannel = 1; //进行adc的通道数量
adc_init(adc1, &adc_initstructure);
//使能adc1
adc_cmd(adc1, enable);
//进行校准
adc_resetcalibration(adc1); //将校准复位
while (adc_getresetcalibrationstatus(adc1) == set); //等待校准复位完成
adc_startcalibration(adc1); //开始校准
while (adc_getcalibrationstatus(adc1) == set); //等待校准完成
}
uint16_t ad_getvalue(void)
{
adc_softwarestartconvcmd(adc1, enable); //软件触发开始转换
while (adc_getflagstatus(adc1, adc_flag_eoc) == reset); //等待转换完成
return adc_getconversionvalue(adc1); //返回转换得到的数值(0~4095)
}
stm32中的直接存储器存取 (dma)
dma 全程 direct memory access (直接存储器存取),功能就是数据复制,优点就是能代替cpu负责数据复制,让cpu空出来处理其他任务。
另外,根据查资料得到,dma的搬运速度没有cpu搬运的速度快的。详细可以看
数据复制方向支持:存储器到存储器、存储器到外设、外设到存储器。其中因为flash一般为只读,所以存储器到存储器为 flash到sram 、sram到sram。
数据宽度:
支持 字节(byte,8位)、半字(halfword,16位)、字(word,32位),支持不同宽度的数据复制,复制对齐为低位对齐。例如:半字(0x1122)复制到字节,则会把低八位复制过去,结果为0x22;半字(0x1122)复制字,则会把半字复制到字的低位,结果为0x00001122。
地址自增:
模式:正常模式(复制完就停下)、循环模式(复制完重新开始,循环模式不可用于存储器到存储器)
dma1的请求对应通道:
外设 | 通道1 | 通道2 | 通道3 | 通道4 | 通道5 | 通道6 | 通道7 |
---|---|---|---|---|---|---|---|
adc1 | adc1 | ||||||
spi/i2s | spi1_rx | spi1_tx | spi/i2s2_rx | spi/i2s2_tx | |||
usart | usart3_tx | usart3_rx | usart1_tx | usart1_rx | usart2_rx | usart2_tx | |
i2c | i2c2_tx | i2c2_rx | i2c1_tx | i2c1_rx | |||
tim1 | tim1_ch1 | tim1_ch2 | tim1_tx4 tim1_trig tim1_com | tim1_up | tim1_ch3 | ||
tim2 | tim2_ch3 | tim2_up | tim2_ch1 | tim2_ch2 tim2_ch4 | |||
tim3 | tim3_ch3 | tim3_ch4 tim3_up | tim3_ch1 tim3_trig | ||||
tim4 | tim4_ch1 | tim4_ch2 | tim4_ch3 | tim4_up |
dma2的请求对应通道:
外设 | 通道1 | 通道2 | 通道3 | 通道4 | 通道5 |
---|---|---|---|---|---|
adc3 | adc3 | ||||
spi / i2s3 | spi i2s3_rx | spi i2s3_tx | |||
uart4 | uart4_rx | uart4_tx | |||
sdio | sdio | ||||
tim5 | tim5_ch4 | tim5_ch3 tim5_up | tim5_ch2 | tim5_ch1 | |
tim6 / dac通道1 | tim6_up dac通道1 | ||||
tim7 / dac通道2 | tim7_up dac通道2 | ||||
tim8 | tim8_ch3 tim8_up | tim8_ch4 tim8_trig tim8_com | tim8_ch1 | tim8_ch2 |
中断与标志位:
中断事件 | 事件标志位 | 使能控制位 | y=dma,x=通道 |
---|---|---|---|
传输过半 | htif | htie | dmay_flag_htx |
传输完成 | tcif | tcie | dmay_flag_tcx |
传输错误 | teif | teie | dmay_flag_tex |
dmay_flag_glx:全局标志,一次性控制三个标志位。
/*
dma 内存到内存 例子
*/
uint16_t mydma_size; //用于二次开始的时候重置复制次数
void mydma_init(uint32_t addra, uint32_t addrb, uint16_t size)
{
rcc_ahbperiphclockcmd(rcc_ahbperiph_dma1, enable); //使能dma1的时钟
mydma_size = size; //记录一下,开始复制的时候要设置
dma_inittypedef dma_initstructure;
dma_initstructure.dma_peripheralbaseaddr = addra; //外设基地址,当用存储器到存储器时,可写存储器地址
dma_initstructure.dma_peripheraldatasize = dma_peripheraldatasize_byte; //外设数据宽度
dma_initstructure.dma_peripheralinc = dma_peripheralinc_enable; //外设地址自增
dma_initstructure.dma_memorybaseaddr = addrb; //存储器基地址
dma_initstructure.dma_memorydatasize = dma_memorydatasize_byte; //存储器数据宽度
dma_initstructure.dma_memoryinc = dma_memoryinc_enable; //存储器地址自增
dma_initstructure.dma_dir = dma_dir_peripheralsrc; //数据传输方向:src外设为源地址,dst外设为目标地址
dma_initstructure.dma_buffersize = size; //需要复制次数,总复制长度=数据宽度*复制次数
dma_initstructure.dma_mode = dma_mode_normal; //模式:normal正常模式,circular循环模式
dma_initstructure.dma_m2m = dma_m2m_enable; //是否为存储器到存储器(如果是则只能软件触发开始)
dma_initstructure.dma_priority = dma_priority_medium; //优先级:z'ji
dma_init(dma1_channel1, &dma_initstructure); //配置dma1的通道1,这里因为是存储器到存储器,所以通道可以随便选
//因为还没有给dma使能,因此没有开始转换
}
void mydma_transfer(void)
{
dma_cmd(dma1_channel1, disable); //赋值复制次数之前要失能dma
dma_setcurrdatacounter(dma1_channel1, mydma_size); //赋值复制次数
dma_cmd(dma1_channel1, enable); //使能dma,开始转换
while (dma_getflagstatus(dma1_flag_tc1) == reset); //等待复制完成
dma_clearflag(dma1_flag_tc1); //清除标志位
}
/*
dma 外设到存储器 例子
adc多通道
*/
uint16_t ad_value[4]; //用于保存adc转换完成的结果
void ad_init(void)
{
//使能时钟
rcc_apb2periphclockcmd(rcc_apb2periph_adc1, enable);
rcc_apb2periphclockcmd(rcc_apb2periph_gpioa, enable);
rcc_ahbperiphclockcmd(rcc_ahbperiph_dma1, enable);
//配置adc时钟频率为apb2时钟的6分频
rcc_adcclkconfig(rcc_pclk2_div6);
//配置4个io口
gpio_inittypedef gpio_initstructure;
gpio_initstructure.gpio_mode = gpio_mode_ain;
gpio_initstructure.gpio_pin = gpio_pin_0 | gpio_pin_1 | gpio_pin_2 | gpio_pin_3;
gpio_initstructure.gpio_speed = gpio_speed_50mhz;
gpio_init(gpioa, &gpio_initstructure);
//配置规则组
adc_regularchannelconfig(adc1, adc_channel_0, 1, adc_sampletime_55cycles5);
adc_regularchannelconfig(adc1, adc_channel_1, 2, adc_sampletime_55cycles5);
adc_regularchannelconfig(adc1, adc_channel_2, 3, adc_sampletime_55cycles5);
adc_regularchannelconfig(adc1, adc_channel_3, 4, adc_sampletime_55cycles5);
//初始化adc为连续扫描模式
adc_inittypedef adc_initstructure;
adc_initstructure.adc_mode = adc_mode_independent;
adc_initstructure.adc_dataalign = adc_dataalign_right;
adc_initstructure.adc_externaltrigconv = adc_externaltrigconv_none;
adc_initstructure.adc_continuousconvmode = enable;
adc_initstructure.adc_scanconvmode = enable;
adc_initstructure.adc_nbrofchannel = 4;
adc_init(adc1, &adc_initstructure);
//具体看上面存储器到存储器例子
dma_inittypedef dma_initstructure;
dma_initstructure.dma_peripheralbaseaddr = (uint32_t)&adc1->dr; //外设基地址为adc1的dr寄存器
dma_initstructure.dma_peripheraldatasize = dma_peripheraldatasize_halfword;
dma_initstructure.dma_peripheralinc = dma_peripheralinc_disable;
dma_initstructure.dma_memorybaseaddr = (uint32_t)ad_value;
dma_initstructure.dma_memorydatasize = dma_memorydatasize_halfword;
dma_initstructure.dma_memoryinc = dma_memoryinc_enable;
dma_initstructure.dma_dir = dma_dir_peripheralsrc;
dma_initstructure.dma_buffersize = 4;
dma_initstructure.dma_mode = dma_mode_circular; //循环模式
dma_initstructure.dma_m2m = dma_m2m_disable;
dma_initstructure.dma_priority = dma_priority_medium;
dma_init(dma1_channel1, &dma_initstructure);
dma_cmd(dma1_channel1, enable); //使能时钟,因为非存储器到存储器,所以要硬件请求才能触发开始复制
adc_dmacmd(adc1, enable); //允许adc1可以提交请求触发dma的数据复制
adc_cmd(adc1, enable); //使能adc
//adc校准
adc_resetcalibration(adc1);
while (adc_getresetcalibrationstatus(adc1) == set);
adc_startcalibration(adc1);
while (adc_getcalibrationstatus(adc1) == set);
adc_softwarestartconvcmd(adc1, enable); //软件触发开始转换
//因为adc为连续扫描模式、dma为循环模式,所以只需要触发开始转换后,硬件就会不断得转换并把数据复制到ad_value 数组
}
stm32中的集成电路总线 (i2c/iic)
在stm32中使用i2c有两种方案,一是软件模拟i2c,二是硬件i2c。两种方案各有各的优缺点,因此了解清楚才能选择适合的。
- 软件模拟i2c
- 优点:可以用在任何gpio口;不会发生卡死(最多出错)
- 硬件i2c
- 优点:速度比软件模拟快;容易出现卡死的问题
关于硬件i2c卡死问题具体可以看
- 卡死原因分析:浅谈stm32硬件i2c
- 具体测试结论:stm32 硬件i2c 到底是不是个坑?
硬件i2c的发送流程图:
硬件i2c的接收流程图:
/*
stm32 使用 硬件i2c 作为主机发送/接收 示例代码
*/
#define oled_address 0x78 //定义一个oled模块的从机地址
void i2c_config(void)
{
//使能i2c与gpio时钟
rcc_apb1periphclockcmd (rcc_apb1enr_i2c1en, enable);
rcc_apb2periphclockcmd (rcc_apb2periph_gpiob, enable);
//初始化gpio,配置pb6与pb7为复用开漏输出
gpio_inittypedef gpio_initstructure;
gpio_initstructure.gpio_mode = gpio_mode_af_od;
gpio_initstructure.gpio_speed = gpio_speed_50mhz;
gpio_initstructure.gpio_pin = gpio_pin_6 | gpio_pin_7;
gpio_init (gpiob, &gpio_initstructure);
//开始初始化i2c
i2c_inittypedef i2c_initstructure;
//使用i2c模式,因为stm32的i2c硬件外设支持扩展smbus协议,因此要指定i2c模式
i2c_initstructure.i2c_mode = i2c_mode_i2c;
i2c_initstructure.i2c_acknowledgedaddress = i2c_acknowledgedaddress_7bit; //七位从机地址
i2c_initstructure.i2c_ownaddress1 = 0x11; //自己作为从机时的地址
i2c_initstructure.i2c_ack = i2c_ack_enable; //默认发送应答
//配置时钟线(scl)占空比为低高电平之比为2,仅在i2c的高速模式(100~400 khz)下有效,标准模式下为1:1
//原因是scl低电平时需要变化sda电平,因此需要更多时间
i2c_initstructure.i2c_dutycycle = i2c_dutycycle_2;
//时钟频率,单位hz,400000 => 400khz
i2c_initstructure.i2c_clockspeed = 400000;
i2c_init (i2c1, &i2c_initstructure);
i2c_cmd (i2c1, enable);
}
//封装一个函数用于等待标准事件,包含超时返回,避免卡死
void i2c_waitevent(i2c_typedef* i2cx, uint32_t i2c_event)
{
uint16_t t = 10000;
while(!i2c_checkevent(i2cx, i2c_event) && t-->0);
}
//指定地址写
void i2c_writereg(uint8_t regaddr, uint8_t data)
{
//等待总线不繁忙
while(i2c_getflagstatus(i2c1, i2c_flag_busy));
//生成一个起始信号
i2c_generatestart (i2c1,enable);
i2c_waitevent (i2c1, i2c_event_master_mode_select); //等待ev5
//发送七位从机地址(oled_address)进行寻找从机。i2c_direction_transmitter表示写,会自动设置最低位为1
i2c_send7bitaddress (i2c1, oled_address, i2c_direction_transmitter);
i2c_waitevent (i2c1, i2c_event_master_transmitter_mode_selected); //等待ev6
//发送一个字节(寄存器地址)
i2c_senddata (i2c1, regaddr);
i2c_waitevent (i2c1, i2c_event_master_byte_transmitting); //等待ev8
//发送一个字节(数据)
i2c_senddata(i2c1, data);
i2c_waitevent (i2c1, i2c_event_master_byte_transmitted); //等待ev8_2
//生成停止信号
i2c_generatestop(i2c1, enable);
}
//指定地址读
uint8_t i2c_readreg(uint8_t regaddress)
{
uint8_t data;
//等待总线不繁忙
while(i2c_getflagstatus(i2c1, i2c_flag_busy));
//生成一个起始信号
i2c_generatestart(i2c2, enable);
mpu6050_waitevent(i2c2, i2c_event_master_mode_select); //等待ev5
//发送七位从机地址(oled_address)进行寻找从机。i2c_direction_transmitter表示写,会自动设置最低位为1
i2c_send7bitaddress(i2c2, oled_address, i2c_direction_transmitter);
i2c_waitevent(i2c2, i2c_event_master_transmitter_mode_selected);
//发送一个字节(寄存器地址)
i2c_senddata(i2c2, regaddress);
i2c_waitevent(i2c2, i2c_event_master_byte_transmitted); //等待ev8_2
//再次生成起始信号
i2c_generatestart(i2c2, enable);
i2c_waitevent(i2c2, i2c_event_master_mode_select); //等待ev5
//发送七位从机地址(oled_address)进行寻找从机。i2c_direction_receiver表示读,会自动设置最低位为0
i2c_send7bitaddress(i2c2, oled_address, i2c_direction_receiver);
i2c_waitevent(i2c2, i2c_event_master_receiver_mode_selected); //等待ev6
//需要在接收之前设置为非应答,因为硬件会在接收完后直接发送 应答/非应答,没有等待时间。
i2c_acknowledgeconfig(i2c2, disable);
//生成停止信号(但是会在当前字节传输或在当前起始条件发出后产生停止条件,因此可以提前给)
i2c_generatestop(i2c2, enable);
i2c_waitevent(i2c2, i2c_event_master_byte_received); //等待ev7
data = i2c_receivedata(i2c2); //读取接收到的数据
i2c_acknowledgeconfig(i2c2, enable); //恢复为默认发送应答
return data;
}
stm32中的串行外设接口 (spi)使用
/*
spi使用的示例例子
*/
void spi2_init(void)
{
//使能时钟
rcc_apb2periphclockcmd( rcc_apb2periph_gpiob, enable );
rcc_apb1periphclockcmd( rcc_apb1periph_spi2, enable );
//初始化gpio,配置pb13、pb14、pb15为复用推挽输出
gpio_inittypedef gpio_initstructure;
gpio_initstructure.gpio_pin = gpio_pin_13 | gpio_pin_14 | gpio_pin_15;
gpio_initstructure.gpio_mode = gpio_mode_af_pp;
gpio_initstructure.gpio_speed = gpio_speed_50mhz;
gpio_init(gpiob, &gpio_initstructure);
gpio_setbits(gpiob,gpio_pin_13|gpio_pin_14|gpio_pin_15); //配置pb13、pb14、pb15为上拉
//开始 初始化spi
spi_inittypedef spi_initstructure;
//设置spi单向或者双向的数据模式:spi设置为双线双向全双工
spi_initstructure.spi_direction = spi_direction_2lines_fullduplex;
//设置spi工作模式:设置为主spi
spi_initstructure.spi_mode = spi_mode_master;
//设置spi的数据大小:spi发送接收8位帧结构
spi_initstructure.spi_datasize = spi_datasize_8b;
//串行同步时钟的空闲状态为高电平
spi_initstructure.spi_cpol = spi_cpol_high;
//串行同步时钟的第二个跳变沿数据被采样
spi_initstructure.spi_cpha = spi_cpha_2edge;
//nss信号由硬件(nss管脚)还是软件(使用ssi位)管理:内部nss信号有ssi位控制
spi_initstructure.spi_nss = spi_nss_soft;
//设置波特率预分频的值:波特率预分频值为256
spi_initstructure.spi_baudrateprescaler = spi_baudrateprescaler_256;
//指定数据传输从msb位还是lsb位开始:数据传输从msb位开始
spi_initstructure.spi_firstbit = spi_firstbit_msb;
//crc值计算的多项式
spi_initstructure.spi_crcpolynomial = 7;
spi_init(spi2, &spi_initstructure);
spi_cmd(spi2, enable); //使能spi外设
spi2_readwritebyte(0xff);
}
//设置 spi 的波特率预分频值
void spi2_setspeed(u8 baudrateprescaler)
{
assert_param(is_spi_baudrate_prescaler(baudrateprescaler));
spi2->cr1 &= 0xffc7; //清零位5:3
spi2->cr1 |= baudrateprescaler; //设置spi2速度
spi_cmd(spi2, enable);
}
//发送一个数据并收回一个数据
u8 spi2_readwritebyte(u8 txdata)
{
u8 retry = 0;
//检查指定的spi标志位设置与否:发送缓存空标志位
while (spi_i2s_getflagstatus(spi2, spi_i2s_flag_txe) == reset) {
retry++;
if(retry>200)return 0;
}
spi_i2s_senddata(spi2, txdata); //通过外设spix发送一个数据
retry = 0;
//检查指定的spi标志位设置与否:接受缓存非空标志位
while (spi_i2s_getflagstatus(spi2, spi_i2s_flag_rxne) == reset){
retry++;
if(retry>200)return 0;
}
return spi_i2s_receivedata(spi2); //返回通过spix最近接收的数据
}
stm32中的控制器局域网 (bxcan)使用
stm32中的can架构:
-
设置
- 速率:can总线的速率常用的都是125k到500k(一般使用500k),尽管它的最大速率是1mbps。但明显的是,最大值往往要求环境更加高,导致容易出现问题。
- 工作模式:初始化模式、正常模式、睡眠模式
- 测试模式:静默模式、回环模式、回环静默模式
- 调试模式:当mcu处于调试模式时,cortex-m3核心处于暂停状态,提供配置,可以使bxcan继续正常工作或停止工作(can是异步通讯,因此需要这个)
-
发送:
- 3个发送邮箱:可以配置发送优先级(按写入先后 / 按标识符数值)
- 自动重传:发送失败则自动重新发送,直至成功
-
接收:
-
2个三级深度接收邮箱(fifo):共可以接收6个报文
-
锁定模式:锁定状态下,接收溢出则丢弃;非锁定状态下,接收溢出则覆盖
-
-
过滤器:
- 14个位宽可配置的标识符过滤器组
- 一个位宽可配置为
1个32位掩码模式
/2个32位标识符列表模式
/2个16位掩码模式
/4个16位标识符列表模式
- 一个位宽可配置为
- 过滤模式
- 标识符列表模式:丢弃掉非指定标识符的报文
- 掩码模式:可以指定标识符某些位是非必要的后进行比对
- 14个位宽可配置的标识符过滤器组
测试模式图解:
过滤器:
can_mode_init()
{
//使能时钟
rcc_apb2periphclockcmd(rcc_apb2periph_gpioa, enable);
rcc_apb1periphclockcmd(rcc_apb1periph_can1, enable);
//初始化can_rx为上拉输入
gpio_inittypedef gpio_initstructure;
gpio_initstructure.gpio_pin = gpio_pin_11;
gpio_initstructure.gpio_mode = gpio_mode_ipu;
gpio_initstructure.gpio_speed = gpio_speed_50mhz;
gpio_init(gpioa, &gpio_initstructure);
//初始化can_tx为复用推挽输出
gpio_initstructure.gpio_pin = gpio_pin_12;
gpio_initstructure.gpio_mode = gpio_mode_af_pp;
gpio_init(gpioa, &gpio_initstructure);
// can单元设置
can_inittypedef can_initstructure;
can_initstructure.can_ttcm = disable; //非时间触发通信模式
can_initstructure.can_abom = disable; //软件自动离线管理
can_initstructure.can_awum = disable; //睡眠模式通过软件唤醒(清除can->mcr的sleep位)
can_initstructure.can_nart = enable; //禁止报文自动传送
can_initstructure.can_rflm = disable; //报文不锁定,新的覆盖旧的
can_initstructure.can_txfp = disable; //优先级由报文标识符决定
can_initstructure.can_mode = can_mode_loopback; //模式设置: mode:0,普通模式;1,回环模式;
// 设置波特率 500kmps
can_initstructure.can_prescaler = 4; //预分频系数
can_initstructure.can_sjw = can_sjw_1tq; //重新同步跳跃宽度 can_sjw_1tq ~ can_sjw_4tq
can_initstructure.can_bs1 = can_bs1_9tq; //can_bs1_1tq ~can_bs1_16tq
can_initstructure.can_bs2 = can_bs2_8tq; //can_bs2_1tq ~ can_bs2_8tq
can_init(can1, &can_initstructure);
can_filterinittypedef can_filterinitstructure;
can_filterinitstructure.can_filternumber = 0; //过滤器0,可以为0~13
can_filterinitstructure.can_filtermode = can_filtermode_idmask; //掩码模式
can_filterinitstructure.can_filterscale = can_filterscale_32bit; //32位
can_filterinitstructure.can_filteridhigh = 0x0000; //32位标识符
can_filterinitstructure.can_filteridlow = 0x0000;
can_filterinitstructure.can_filtermaskidhigh = 0x0000; //32位掩码,1:要求一致,0:不限制
can_filterinitstructure.can_filtermaskidlow = 0x0000;
can_filterinitstructure.can_filterfifoassignment = can_filter_fifo0; // 关联到fifo0
can_filterinitstructure.can_filteractivation = enable; // 使能过滤器0
can_filterinit(&can_filterinitstructure); // 滤波器初始化
/*
用于开启中断
can_itconfig(can1, can_it_fmp0, enable); // fifo0消息挂号中断允许
nvic_inittypedef nvic_initstructure;
nvic_initstructure.nvic_irqchannel = usb_lp_can1_rx0_irqn;
nvic_initstructure.nvic_irqchannelpreemptionpriority = 1; // 主优先级为1
nvic_initstructure.nvic_irqchannelsubpriority = 0; // 次优先级为0
nvic_initstructure.nvic_irqchannelcmd = enable;
nvic_init(&nvic_initstructure);
*/
}
//中断函数模板
void usb_lp_can1_rx0_irqhandler(void)
{
canrxmsg rxmessage;
int i = 0;
can_receive(can1, 0, &rxmessage);
for (i = 0; i < 8; i++)
printf("rxbuf[%d]:%d\r\n", i, rxmessage.data[i]);
}
//发送报文,返回0为成功,否则失败
u8 can_send_msg(u8 *msg, u8 len)
{
u8 mbox;
u16 i = 0;
cantxmsg txmessage;
txmessage.stdid = 0x12; //标准标识符
txmessage.extid = 0x12; //设置扩展标示符
txmessage.ide = can_id_standard; //表明为标准帧
txmessage.rtr = can_rtr_data; //表明为数据帧
txmessage.dlc = len; //要发送的数据长度
for (i = 0; i < len; i++) //复制数据到结构体
txmessage.data[i] = msg[i];
mbox = can_transmit(can1, &txmessage); //填入发送邮箱,mbox为被填入的邮箱号
i = 0;
while ((can_transmitstatus(can1, mbox) == can_txstatus_failed) && (i < 0xfff))
i++; //等待发送结束
if (i == 0xfff)
return 1; //超时
return 0;
}
//接收数据查询,成功返回数据长度,没有返回0
u8 can_receive_msg(u8 *buf)
{
u32 i;
canrxmsg rxmessage;
if (can_messagepending(can1, can_fifo0) == 0) //查询邮箱有多少条数据
return 0;
can_receive(can1, can_fifo0, &rxmessage); //读取数据
for (i = 0; i < 8; i++)
buf[i] = rxmessage.data[i];
return rxmessage.dlc;
}
发表评论