STM32学习笔记(STM32F103C8T6)
STM32简介及工具软件安装
STM32简介
芯片命名及手册
下载烧录
STM32单片机支持3种程序下载方式
- ISP串口下载(使用USB-TTL接PA9、PA10)
- SWD下载(使用ST-LINK接PA13、PA14)
- JTAG下载(使用JLINK接PA13、PA14、PA15、PB3、PB4)
虽然有三种方式,但是我个人一般是使用st-link的,所以下面主要介绍这一个
ISP下载(串口)
使用ISP串口下载前,将单片机上电之前需要先用跳线帽把BOOT0
短接到1
的位置,BOOT1
短接到0
的位置,即系统存储器模式,然后才能通过串口下载程序。ISP串口下载完成后断电,在单片机上电之前需要先用跳线帽把BOOT0
短接到0
的位置,即主闪存存储器模式。
下载器GND与单片机GND相连,下载器3.3V与单片机3.3V相连(或者下载器5V与单片机VIN相连)、下载器RXD与单片机PA9(U1TX)相连,下载器TXD与单片机PA10(U1RX)相连
SWD下载(st-link)
使用SWD接口下载只需要连接3.3V、GND、SWDIO(PA13)
、SWCLK(PA14)
、RST
(非必要),可以从淘宝购买ST-LINK
下载器。使用SWD接口除了可以烧录程序外,还可以实现在线仿真(debug),仿真过程可以监视寄存器等数据,非常适合软件开发(找问题)。ST-LINK/V2
只支持给自家的STM32和STM8烧录程序,不支持为其他公司的单片机烧录程序(即使同样搭载Cortex-M3
内核)
如果使用STLink
,其上的LED指示灯用于提示当前的工作状态:
- LED 闪烁红色:
STLink
已经连接至计算机。 - LED 保持红色:计算机已经成功与
STLink
建立通信连接。 - LED 交替闪烁红色和绿色:数据正在传输。
- LED 保持绿色:最后一次通信是成功的。
- LED 为橘黄色:最后一次通信失败。
JTAG下载
这种方式很少使用,不再详细叙述
如果我们不需要使用JTAG下载,但GPIO资源紧张或PCB设计时已经使用了这些第一功能为JTAG的引脚,那么我们就需要关闭JTAG。比如说我要使用GPIOA15作为GPIO口,那么代码层面需要这样实现:
1
2
3
4
5
6
7 GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);//使能PORTA时钟
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE);// 关闭JTAG但使能SWD
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;//PA15
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIO
编程环境
使用arduino编程
突然得知stm32可以用arduino编程,喜出望外hhh
添加开发板管理器地址
1 | http://dan.drown.org/stm32duino/package_STM32duino_index.json |
然后就可以找到STM32F103C
来给我的板子编程了。
使用keil编程
Keil uVision
:编程工具,可以在官网注册获取下载链接,可能需要自行搜索破解,虽然不破解也能正常使用基础功能。STM32 ST-LINK Utility
:配套ST-LINK
一起使用的烧录工具,包含ST-Link
驱动,同样可以在官网下载,我也copy的一份在网盘上
其中如果使用后者进行下载而不是keil自带的下载按钮,需要设置keil编译后输出.hex
文件:
在STM32 ST-LINK Utility
中,首先连接芯片(Tarage -> connect或直接点击连接快捷按钮)
,然后打开hex
文件(也可以直接讲hex文件拖动到FLASH区域)
,最后就可以下载程序(Taraget -> Program,也可以直接点击下载快捷按钮)
。弹出信息确认窗口,如hex文件路径、验证方式等,确认信息无误后点击Start
开始下载程序,出现Verification…OK
,说明下载成功。
在keil环境下编程(标准库)
新建工程
首先选择开发板芯片,我的是stm32f103c8
添加启动文件等,这部分文字说明比较麻烦(暂时懒得写了),建议参考这个视频
添加main.c
文件,之后就可以在main文件中写代码了,写完可以编译一下,如果输出正确就表示环境配置没问题
基础操作
GPIO高低电平输出
磨磨唧唧的讲解
在GPIO输出之前要先对要操作的GPIO进行配置,下面这个程序可以连续将PC13这个引脚拉低拉高:
1 |
|
下面来解释一下这个程序:
A:定义GPIO的初始化类型结构体
1 | GPIO_InitTypeDef GPIO_InitStructure; |
此结构体的定义是在stm32f10x_gpio.h
文件中,其中包括3个成员:
uint16_t GPIO_Pin;
来指定GPIO的哪个或哪些引脚GPIOSpeed_TypeDef GPIO_Speed;
GPIO的速度配置,对应3个速度:10MHz、2MHz、50MHzGPIOMode_TypeDef GPIO_Mode;
为GPIO的工作模式配置,即GPIO的8种工作模式。- 输入浮空
GPIO_Mode_IN_FLOATING
- 输入上拉
GPIO_Mode_IPU
- 输入下拉
GPIO_Mode_IPD
- 模拟输入
GPIO_Mode_AIN
- 具有上拉或下拉功能的开漏输出
GPIO_Mode_Out_OD
- 具有上拉或下拉功能的推挽输出
GPIO_Mode_Out_PP
- 具有上拉或下拉功能的复用功能推挽
GPIO_Mode_AF_PP
- 具有上拉或下拉功能的复用功能开漏
GPIO_Mode_AF_OD
- 输入浮空
B:使能GPIO时钟
1 | RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); |
此函数是在stm32f10x_rcc.c
文件中定义的。其中第一个参数指要打开哪一组GPIO的时钟,取值参见stm32f10x_rcc.h
文件中的宏定义,第二个参数为打开或关闭使能,取值参见stm32f10x.h
文件中的定义,其中ENABLE
代表开启使能,DISABLE
代表关闭使能。
1 | void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState); |
C:设置
GPIO_InitTypeDef
结构体三个成员的值
1 | GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; |
D:初始化GPIO
1 | GPIO_Init(GPIOC, &GPIO_InitStructure); |
E:GPIO电平输出
函数就是置位GPIO,即让相应的GPIO输出高电平
1 | GPIO_ResetBits(GPIOC,GPIO_Pin_13); |
很多网上找到的程序也会这样做,在文件开头写
1 |
然后在调用时候就可以直接写
1 | LED3_ON; |
直接上代码(LED闪烁)
1 |
|
这里面的延时函数来自up江协科技
Delay.c
Delay.h
GPIO输入
这一部分原理和上面几乎一样,通过GPIO_ReadInputDataBit()
读取GPIO输入
1 | //初始化引脚 |
此外,对于输出的引脚,也可以使用GPIO_ReadOutputDataBit()
读取输出,比如这样翻转输出电平:
1 | void LED_Turn(void) |
中断
中断涉及的结构如下图:
这一流程也就是我们配置中断的流程
配置GPIO
1 | RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); |
配置AFIO选择引脚
1 | RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //打开AFIO的时钟 |
配置EXTI
1 | EXTI_InitTypeDef EXTI_InitStructure; //定义EXTI初始化结构体 |
其中触发方式包含:
1 | //下降沿 |
配置NVIC
1 | NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断分组,两位抢占优先级两位响应优先级 |
中断函数
stm32的中断函数名字是固定的,比如这里是EXTI15_10_IRQn通道的函数:
1 | void EXTI15_10_IRQHandler(void){ |
实例
这个程序可以实现PB11下降沿中断时反转PC13引脚的输出:
1 |
|
定时器
定时中断
定时中断基本结构:
操作流程
1 | RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //tim2是APB1总线的外设,开启时钟 |
下面是中断函数
1 | void TIM2_IRQHandler(void){ |
代码操作
这个程序可以让PC13每秒亮灭一次
1 |
|
PWM输出
定时器配置和上一节一样,只不过不需要中断配置了
1 | RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); |
之后修改pwm占空比就可以使用这个函数:Compare为CCR
1 | TIM_SetCompare1(TIM2, Compare); |
TIM输入捕获
串口通讯
keil调试
使用st-link连接上stm32后,点击这个按钮,进入调试模式
代码窗口左侧灰色区域可以设置断点,左上角可以设置单步运行等功能。
如果需要查看变量的值需要打开View > Watch Windows > watch 1
,即可输入变量名查看变量的实时值(注意必须是全局变量)
使用cubemx自动配置寄存器(HAL库)
工程基本配置:以stm32f103c8t6为例
设置外置时钟源和串口调试:
时钟源选择外置8MHz和32.768MHz,时钟树如下:
修改工程名称IDE代码输出内容等:
点灯:定时器中断闪烁
这里使用TIM3触发中断然后反转PC13电平
cubemx中设置TIM3的PSC为7199,72M时钟频率分频到10kHz,然后ARR设置为9999即为每秒中断1次,生成工程后在main.c
添加定时器开启和中断函数:
1 | HAL_TIM_Base_Start_IT(&htim3); |
1 | void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) // 该函数在 stm32f1xx_hal_tim.c 中定义为弱函数(__weak),由用户再定义 |
编译下载即可看到PC13指示灯闪烁
点灯:输出PWM
使用TIM2的Channel1,配置如下:
72Mhz的时钟不分频,ARR设置为999即为频率72kHz,CH1的Pluse为100,因此占空比为10%
然后在main()函数中启动输出
1 | HAL_TIM_PWM_Start (&htim2,TIM_CHANNEL_1); |
即可看到示波器显示:
oled显示
这里使用了ssd1306驱动库
cubemx配置i2c1,使用fastmode:
然后在keil添加如下文件:
fonts.c
在main.c的while(1)之前添加:
1 | SSD1306_Init (); // initialise the display |
即可看到
下面的代码将会显示数字并每秒加一
1 | SSD1306_Init (); |
缩短delay时间估计实际刷新用时大约25ms,因此这个读秒并不准确,应该用定时器中断来计算num
GPIO输入中断
首先设置中断引脚
设置触发模式和内部上下拉
设置nvic开启中断
生成代码后添加中断触发的回调函数。下面代码实现内容是按键消抖,使用TIM3定时器100ms一次中断计时从而避免阻塞
1 | /* USER CODE BEGIN 4 */ |
注意:如果在中断中使用了HAL_GetTick()
和HAL_Delay()
这类函数,要调整中断优先级,否则会在GPIO中卡死,GPIO中断优先级要比Time base
的要低,也就是Preemption Priority
更大
ADC(DMA)
在cubemx中配置ADC1,并开启DMA。这里我开启了10个通道的循环扫描输入,DNA设置为自动连续转运,内存地址自增
使用DMA转运到内存的100位数组,然后取平均值(只读了两个通道)然后在屏幕显示
1 | uint32_t ADC_Value[100]; |
串口通讯
串口发送(中断)
在cubemx配置串口参数后即可使用下面的HAL库函数直接发送串口数据
1 | uint8_t data[] = "Hello, UART!"; |
串口接收(中断)
首先定义中断回调处理函数,这里函数的内容是将发来的内容发回去
注意下面这种方式每次最多发送6个字符,否则每第七个字符会被丢弃
1 | void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) |
然后在main()函数进入while(1)循环之前开启接收
1 | HAL_UART_Receive_IT(&huart1, &rxData, 1); |
以及别忘了最开始定义一个存储数据的全局变量
1 | uint8_t rxData; |
串口发送(DMA)
串口接收(DMA)
串口接收数据解析存储
通过i2c读取传感器数据
mpu6050示例
这部分内容参考了这篇文章,对里面一些内容做了一些小的修改和解释
关于mpu6050的地址
值取决于引脚 AD0。该引脚位于传感器的分线板上接入 GND。这意味着设备的7位Slave地址为
0x68
。但是我们需要为 STM32 HAL 提供 8 位地址,因此我们将这个 7 位地址向左移动 1 位,0x68<<1 = 0xD0
。如果AD0接入高电平,那么地址将会是0x69
初始化mpu6050
首先通过读取
WHO_AM_I
(0x75
)寄存器来检查传感器是否响应。如果传感器响应0x68
,则意味着通信正常然后配置
PWR_MGMT_1 (0x6B)
”寄存器,我们将此register
重置为0
。在此过程中,我们将:- 选择 8 MHz 的内部 clock source。
- Temperature sensor (温度传感器) 将被启用。
- 将启用睡眠模式和唤醒模式之间的 CYCLE。
- SLEEP 模式将被禁用。
- 此外,我们不执行 RESET。
设置 数据输出率 or 采样率.这可以通过写入
SMPLRT_DIV (0x19)
寄存器来完成。此 register 指定陀螺仪输出速率的分频器,用于生成 MPU6050 的Sample Rate
。为了获得 1KHz 的采样率,我们将SMPLRT_DIV
值设置为 7修改
GYRO_CONFIG (0x1B)
和ACCEL_CONFIG (0x1C)
寄存器来配置Accelerometer
和Gyroscope
寄存器,将0x18
写入这两个寄存器将在 Register 中设置满量程范围,并禁用自检- 加速度计量程设置:
- 0x00: ±2g
- 0x08: ±4g
- 0x10: ±8g
- 0x18: ±16g
- 陀螺仪量程设置:
- 0x00: ±250°/s
- 0x08: ±500°/s
- 0x10: ±1000°/s
- 0x18: ±2000°/s
例如16进制
0x10
和0x18
对应二进制10000
和11000
,因此按照手册去掉后三位无效数字,前两位是二进制10
和11
,也就是10进制的2
和3
,也就是手册描述的两个设置选项- 加速度计量程设置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void MPU6050_Init (void);
void MPU6050_Init(void) {
uint8_t check;
uint8_t data;
// 检查设备ID
HAL_I2C_Mem_Read(&hi2c1, 0x68 << 1, 0x75, 1, &check, 1, 1000);
if (check == 0x68) {
// 唤醒
data = 0;
HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x6B, 1, &data, 1, 1000);
// 设置采样率
data = 0x07;
HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x19, 1, &data, 1, 1000);
// 设置加速度计量程
data = 0x18;
HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x1C, 1, &data, 1, 1000);
// 设置陀螺仪量程
data = 0x18;
HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x1B, 1, &data, 1, 1000);
}}读取 MPU6050 数据
根据手册给出的寄存器地址,传感器数据在0x3B
到0x48
之间,直接读取这一段内容,将每个参数的较高的 8 位向左移动,并和低 8 位的结果相加,得到完整的16位数据,然后根据选择的量程和分辨率转换成以g
为单位的数值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void MPU6050_Read(float *Ax,float *Ay,float *Az,float *Gx,float *Gy,float *Gz);
void MPU6050_Read(float *Ax,float *Ay,float *Az,float *Gx,float *Gy,float *Gz) {
uint8_t data[14];
HAL_I2C_Mem_Read(&hi2c1, 0x68 << 1, 0x3B, 1, data, 14, 1000);
int16_t Accel_X_RAW = (data[0] << 8) | data[1];
int16_t Accel_Y_RAW = (data[2] << 8) | data[3];
int16_t Accel_Z_RAW = (data[4] << 8) | data[5];
int16_t Gyro_X_RAW = (data[8] << 8) | data[9];
int16_t Gyro_Y_RAW = (data[10] << 8) | data[11];
int16_t Gyro_Z_RAW = (data[12] << 8) | data[13];
*Ax = (float)Accel_X_RAW/2048.0;
*Ay = (float)Accel_Y_RAW/2048.0;
*Az = (float)Accel_Z_RAW/2048.0;
*Gx = (float)Gyro_X_RAW/16.4;
*Gy = (float)Gyro_Y_RAW/16.4;
*Gz = (float)Gyro_Z_RAW/16.4;
}调用这两个函数并串口发送就好了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18MPU6050_Init();
float Data_mpu6050[6]={0.0,0.0,0.0,0.0};
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
MPU6050_Read(&Data_mpu6050[0], &Data_mpu6050[1], &Data_mpu6050[2], &Data_mpu6050[3], &Data_mpu6050[4], &Data_mpu6050[5]);
sprintf (bufnum, "Ax=%07.4f\nAy=%07.4f\nAz=%07.4f\nGx=%07.1f\nGy=%07.1f\nGz=%07.1f\n", Data_mpu6050[0],Data_mpu6050[1],Data_mpu6050[2],Data_mpu6050[3],Data_mpu6050[4],Data_mpu6050[5]);
HAL_UART_Transmit(&huart1, bufnum, sizeof(bufnum) - 1, 1000);
Delay_ms(100);
/* USER CODE END WHILE */
}