多图杀猫。实验环境为Windows。
咳咳,本次实验感觉环境好难配啊,实验倒是感觉起来容易一些(果然有攻略就是舒服)。
实验准备 - 点灯
1. STM32CubeMX
接下来把文末下载区的几个全部先下载下来备用。
首先安装主程序STM32CubeMX,就一路下一步下一步下一步就好了,看得懂的比如说安装目录之类的改一下就好了,不需要防备360安全套餐、百度全家桶等。
打开程序之后界面长这样。
萌新上来什么都不懂,跟着教程直接新建工程,选择了核心板的型号STM32F103C8之后直接点OK。
点击Project选项卡中的setting,进入到项目设置。填写完项目名称、位置以及工具链。在图中,我选的是MDK-ARM V5,下载列表中有相关工具下载(Keil)。
全部设置好之后,点击下方OK按钮。说时迟那时快,只听砰的一声,一个讨厌的警示框跳出来了。告诉我们,有个依赖没有满足。
此时可以直接点击Yes开启自动下载模式,全自动安装,你值得拥有!
但是由于各种原因断在70M这个位置两次之后我心灰意冷选择放弃,各种查找资料之后发现还可以进行本地安装。
首先是从Help选型卡中的Install New Libraries进入库管理界面。
此时,左下角有一个** From Local ... **按钮,将下载的V1.3.0包以及V1.3.1补丁顺序打入即可。安装完毕后可以看到安装的是当前最新版本。
接下来为了点亮小灯,需要找一个引脚输出高电平形成电流。在此,我选择将PA9接口置为GPIO_Output模式。多说一句,实际上这个地方的配置不是必须的,因为这里的配置最终也是由MX转译成为C的代码,而在Keil中才是真正的编译,即可以在代码中直接编写相应的流程。不过,话说回来,能自动干的东西为啥要码代码。
而后,点击代码生成按钮。
生成结束后后会在项目的文件夹下生成一个由设置所决定的工程文件夹。而Open Project目前点了还没有作用,因为还没有装相应的软件。
至此,STM32CubeMX的流程就结束了。
2. Keil
STM32CubeMX生成出了整个工程的目录,但是需要一个软件将其打开。我选择使用Keil v5(其实是先选的v5,上面的Project Setting中有设置)。
Keil的安装就没有上面那个这么麻烦,直接下一步下一步下一步,注册码也有注册机直接可以搞定。过程一如参考资料里的安装教程。
话接上集,直接点击Open Project按钮,打开项目工程文件。中间还会需要安装一个依赖包,继续同意即可。
可以从图中看到,STM32CubeMX已经帮我们生成好了一系列的基础文件,从中进行修改以满足我们的要求即可。
由于我们在MX中已经有进行过设置,修改了PA9为输出引脚,在代码中就得到了体现。下方即为生成的GPIO初始化代码。
main函数中调用了GPIO初始化函数之后,PA9就开始持续输出高电平。而为了让小灯闪烁,自然不能一直高电平下去,而是需要输出一个矩形波。
至此,代码已经全部写完。而在下载之前,需要在Flash -> Configure Flash Tools -> Utilities中,将Use Debug Driver的勾去掉,转而使用ST-LINK V/2。
按F7进行模块编译,按F8下载到板子吧!
值得一提的是,下载之后,并不会立刻有反应。而是需要断电重启或者使用板子上带的RST按钮进行重置之后程序才会正式开始运行。
注意注意,我图中是将小灯直接连入GND以及VCC,但是有的小灯比较脆弱,这么做会直接烧掉。所以稳妥的做法应该在连接电路中再加一个电阻。s
实验步骤
0. 连接图
按照Lab3攻略上说的连好图,先假装自己连对了确实连对了。
其中,ST-LINK接四根线3.3V、GND、SWDIO、SWCLK分别对应STM32板子上的3.3V、GND、DIO、DCLK。此为烧录用的线路。而PA9、PA10为串口通信所用的线路。所以图中使用了两个USB口。
面包板上线的连接方式为从引脚出来之后经过按钮到GND。
1. UART串口输出
首先进入STM32CubeMX。按照实验中的要求,在右侧芯片设置中,将PA12、PA11定为输入(接按钮),PA10、PA9分别定为TX、RX(接电脑串口)。
同时,在左侧的配置中,将USART1的模式定为Half-Duplex。这步所对应生成的代码与实验攻略中的代码略有差别。但是在不指定模式的情况下,PA9以及PA10会被认为是GPIO_Output而与PA11一起进行初始化,而不是TX、RX口。所以在此选择一个模式。
配置完毕,接下来是代码生成,点击按钮静静等待即可。
少女祈祷中。。。
代码生成完之后,基本的函数已经都有了。但是还需要自己手动填写一些代码,如UART0_Init()等。而stm32f1xx_hal_msp.c中所需填写的函数与Half-Duplex模式一致,所以不需要进行大幅度改动。只需要将攻略的几行代码填入即可。
void UART0_Init(UART_HandleTypeDef* UartHandle){
UartHandle->Instance = USART1;
UartHandle->Init.BaudRate = 9600;
UartHandle->Init.WordLength = UART_WORDLENGTH_8B;
UartHandle->Init.StopBits = UART_STOPBITS_1;
UartHandle->Init.Parity = UART_PARITY_NONE;
UartHandle->Init.HwFlowCtl = UART_HWCONTROL_NONE;
UartHandle->Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(UartHandle);
}
int main(void) {
...
UART_HandleTypeDef UartHandle;
UART0_Init(&UartHandle);
while (1) {
HAL_UART_Transmit(&UartHandle, (uint8_t*)"press\r\n", 7, 500);
HAL_Delay(100);
}
}
HAL_UART_Transmit有4个参数,第一个参数是串口的句柄,第二个参数是一个二进制数组(char*),第三个参数是要发送的数据长度,第四个是发送超时的判定时间。
2. 接入开关
在0中已经将线连好。
3. 检测按钮被按下
由于按钮接地,所以,当按钮被按下时,PA11应该可以检测到一个低电平的输入。编写程序的时候,可以根据这一点,读取PA11的引脚值,并据此判断。
但是,在实验过程中结果并不理想。测试结果如下。
注意到按钮的状态是不断变化的,而我并没有去接触按键。这种情况可能是由于电路本身不精密所导致的。~~~~(Update:做了第4项实验之后,发现同样的接法PA12没问题,看来主要是因为按钮的问题才导致了抖动)(Update: 突然觉得可能是面包板的问题)
硬件难改,但实验也不能这么停止。仿照计算机组成实验的思想,尝试着使用按键防抖动的方法检测按键是否被按下。具体方法是检测按键被稳定按下某几个周期之后,才表示真正检测到了按键事件。
但是实际测试结果并不理想,由图可以看出。我依然没有按任何的按键,而串口却一直有输出。
思考了一下,发现可能是由于两次检测时间间隔太短,导致跳变的引脚值被记录多次。此时,使用延时检测即可,即两次检测之间间隔拉大。
下方代码为最终版本,即加了anti_jitter即HAL_Delay的结果。
#define MAX_BITCOUNT 0xff
#define CHECK_DELAY 10
void anti_jitter(int *bitcount, int state){
*bitcount <<= 1;
*bitcount &= MAX_BITCOUNT;
*bitcount += state & 1;
}
int main(void)
{
int total, bitcount;
char str[64];
int send = 0;
// something for initialization.
total = 0; bitcount = MAX_BITCOUNT ;
while (1) {
int cnt;
GPIO_PinState state;
state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11);
HAL_Delay(CHECK_DELAY);
anti_jitter(&bitcount, state);
if (bitcount == 0){
if (!send){
send = 1;
total++;
cnt = sprintf(str, "Press 11 %d times\r\n", total);
HAL_UART_Transmit(&UartHandle, (uint8_t*)str, cnt, 500);
}
}else{
send = 0;
}
}
}
最终代码能够做到无误检测,同时在按键被按下之后,响应时间较短。
4. PA12中断响应
PA12引脚的下降沿触发将会触发中断,进入函数EXTI15_10_IRQHandler,此时在函数中调用HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12)表示查看PA12的值,如果符合条件,则触发HAL_GPIO_EXTI_Callback函数。
在callback函数中,将检测标志位置1即可被while循环中的if识别并输出。代码如下。
// ---------- stm32f1xx_it.c ----------
void EXTI15_10_IRQHandler(void){
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12);
}
// ---------- main.c ----------
void NVIC_Init(){
// 使能中断
HAL_NVIC_SetPriority(EXTI15_10_IRQn,0,0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}
int PA12count = 0, PA12flag = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if (GPIO_Pin == GPIO_PIN_12){
PA12flag = 1;
PA12count ++;
}else{
UNUSED(GPIO_Pin);
}
}
int main(void) {
int total, bitcount;
char str[64];
int send = 0;
NVIC_Init();
......
while (1) {
int cnt;
......
if (PA12flag == 1){
PA12flag = 0;
cnt = sprintf(str, "Press 12 %d times\r\n", PA12count);
HAL_UART_Transmit(&UartHandle, (uint8_t*)str, cnt, 500);
}
}
}
void MX_GPIO_Init(void) {
......
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
......
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
5. 定时器中断
定时器中断的实现思路与引脚涉及的中断基本一致。不同的是需要设置中断触发的时间。因为不同于外部中断,时钟中断是内部触发,所以需要预先设定好触发时间。
而后,同样的,需要覆写中断触发函数TIM3_IRQHandler,而后在其中对时钟进行判断后触发HAL_TIM_PeriodElapsedCallback。并在callback中真正处理逻辑。
// ---------- stm32f1xx_hal_msp.c ----------
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim){
__TIM3_CLK_ENABLE();
}
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* htime){
__TIM3_CLK_DISABLE();
}
// main.c
void NVIC_Init(){
...
HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
TIM_HandleTypeDef TIM_Handle;
int TIMflag = 0;
void TIM3_IRQHandler(void){
HAL_TIM_IRQHandler(&TIM_Handle);
}
void TIM_Init(){
TIM_Handle.Instance = TIM3;
TIM_Handle.Init.Prescaler = 8000;
TIM_Handle.Init.CounterMode = TIM_COUNTERMODE_UP;
TIM_Handle.Init.Period = 199;
HAL_TIM_Base_Init(&TIM_Handle);
HAL_TIM_Base_Start_IT(&TIM_Handle);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
TIMflag = 1;
}
int main(void){
......
total = 0; bitcount = MAX_BITCOUNT;
while (1) {
int cnt;
GPIO_PinState state;
HAL_Delay(CHECK_DELAY);
state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11);
anti_jitter(&bitcount, state);
if (bitcount == 0){
if (send != 2){
send = 1;
}
}else if (send == 2){
send = 0;
}
if (TIMflag == 1){
TIMflag = 0;
if (send == 1){
send = 2;
total++;
cnt = sprintf(str, "Press 11 %d times\r\n", total);
HAL_UART_Transmit(&UartHandle, (uint8_t*)str, cnt, 500);
}
if (PA12flag == 1){
PA12flag = 0;
cnt = sprintf(str, "Press 12 %d times\r\n", PA12count);
HAL_UART_Transmit(&UartHandle, (uint8_t*)str, cnt, 500);
}
}
}
}
6. 自行车码表
码表有两个模式,里程模式以及速度模式,两个模式有区别也有联系。
首先是最基础的里程模式,该模式只对应PA12按钮的事件。在每次PA12被按下(轮子走了一圈)的时候算好里程即可,或者使用圈数*轮子周长的方式也行。在本程序中,将轮子周长表示为3.14m。
其次是速度模式。速度模式不能单单使用总里程/总时间,这样得到的总速度是没有意义的,而近似实时的速度计算法应该是通过计算在一定时间内所行驶的里程数推算出短时间内的速度。在本程序中,使用1.6s作为计算的时间长度,即8个周期。
代码清单
main.c
#define MAX_BITCOUNT 0xff
#define CHECK_DELAY 10
#define PERIMETER 3.14
UART_HandleTypeDef UartHandle;
TIM_HandleTypeDef TIM_Handle;
int cnt;
int total, bitcount, dispMode = 0;
int PA12count = 0, PA12flag = 0;
int TIMflag = 0, TIMcount = 0;
float RouteTick[10];
char str[64];
int send = 0;
void UART0_Init(){
UartHandle.Instance = USART1;
UartHandle.Init.BaudRate = 9600;
UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
UartHandle.Init.StopBits = UART_STOPBITS_1;
UartHandle.Init.Parity = UART_PARITY_NONE;
UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
UartHandle.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(&UartHandle);
}
void NVIC_Init(){
HAL_NVIC_SetPriority(EXTI15_10_IRQn,0,0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
void anti_jitter(int *bitcount, int state){
*bitcount <<= 1;
*bitcount &= MAX_BITCOUNT;
*bitcount += state & 1;
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if (GPIO_Pin == GPIO_PIN_12){
PA12flag = 1;
PA12count ++;
}else{
UNUSED(GPIO_Pin);
}
}
void EXTI15_10_IRQHandler(void){
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12);
}
void TIM3_IRQHandler(void){
HAL_TIM_IRQHandler(&TIM_Handle);
}
void TIM_Init(){
TIM_Handle.Instance = TIM3;
TIM_Handle.Init.Prescaler = 8000;
TIM_Handle.Init.CounterMode = TIM_COUNTERMODE_UP;
TIM_Handle.Init.Period = 199;
HAL_TIM_Base_Init(&TIM_Handle);
HAL_TIM_Base_Start_IT(&TIM_Handle);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
TIMflag = 1;
TIMcount++;
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
UART0_Init();
NVIC_Init();
TIM_Init();
total = 0; bitcount = MAX_BITCOUNT;
dispMode = 0;
while (1) {
int cnt;
GPIO_PinState state;
HAL_Delay(CHECK_DELAY);
state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11);
anti_jitter(&bitcount, state);
if (bitcount == 0){
send = 1;
}else{
if (send == 2){
send = 0;
}
}
if (send == 1){
send = 2;
dispMode = 1 - dispMode;
}
if (TIMflag == 1){
int head = (TIMcount - 1) % 8;
int nexHead = (head + 1) % 8;
float route = PA12count * PERIMETER;
RouteTick[head] = route;
TIMflag = 0;
if (dispMode == 0){
cnt = sprintf(str, "You traveled %.2f m\r\n", route);
}else{
float inThisTime = RouteTick[head] - RouteTick[nexHead];
cnt = sprintf(str, "You traveled in %.2f m/s\r\n", inThisTime / 1.6);
}
HAL_UART_Transmit(&UartHandle, (uint8_t*)str, cnt, 500);
}
}
}
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pins : PA11 PA12 */
GPIO_InitStruct.Pin = GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
stm32f1xx_hal_msp.c
void HAL_MspInit(void)
{
__HAL_RCC_AFIO_CLK_ENABLE();
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* System interrupt init*/
/* MemoryManagement_IRQn interrupt configuration */
HAL_NVIC_SetPriority(MemoryManagement_IRQn, 0, 0);
/* BusFault_IRQn interrupt configuration */
HAL_NVIC_SetPriority(BusFault_IRQn, 0, 0);
/* UsageFault_IRQn interrupt configuration */
HAL_NVIC_SetPriority(UsageFault_IRQn, 0, 0);
/* DebugMonitor_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DebugMonitor_IRQn, 0, 0);
/* SysTick_IRQn interrupt configuration */
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim){
__TIM3_CLK_ENABLE();
}
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* htim){
__TIM3_CLK_DISABLE();
}
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
GPIO_InitTypeDef GPIO_InitStruct;
if(huart->Instance==USART1)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_USART1_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
void HAL_UART_MspDeInit(UART_HandleTypeDef* huart)
{
if(huart->Instance==USART1)
{
__HAL_RCC_USART1_CLK_DISABLE();
__HAL_RCC_USART1_FORCE_RESET();
__HAL_RCC_USART1_RELEASE_RESET();
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);
}
}
7. PA11的按钮也采用中断方式检测,主程序只检测标识做串口发送
有了PA12的经验,修改PA11按钮的检测方式也比较容易。(假设PA11没有抖动问题)
首先,需要在初始化PA11的时候,将其设置为中断触发,我设置的是下降沿触发。
GPIO_InitStruct.Pin = GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
而后,在中断处理函数内添加对PA11的处理。
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if (GPIO_Pin == GPIO_PIN_12){
PA12flag = 1;
PA12count ++;
}else if(GPIO_Pin == GPIO_PIN_11){
PA11flag = 1;
}else{
UNUSED(GPIO_Pin);
}
}
void EXTI15_10_IRQHandler(void){
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_11);
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12);
}
最后,只需要在主循环中对PA11flag进行检测即可。
if (PA11flag == 1){
PA11flag = 0;
dispMode = 1 - dispMode;
}
当然,由于PA11flag只是涉及到状态的改变,可以直接在中断触发函数中进行模式的切换。
参考资料:
- 第3次实验的指导文档 by 杨凯
- [分享] [練習]以STM32CubeMX+Keil 成功點燈 -- 比較 STM32 與 Arduino 的差異
- STM32CubeMX的固件库的离线安装方法
- keil mdk v5.11 官方最新版 这个链接是为了安装教程,没有下载过
- STM32F103 Blink LED (using Keil and STMCubeMX)
- STM32学习笔记之EXTI(外部中断)
- stm32库函数详解