有关单片机的阻塞与非阻塞
大家好今天是2026年8月2日建军节刚过1天。今天我想与大家分享的是有关单片机的阻塞与非阻塞。大家都知道裸机开发和实时操作系统开发吧这两个开发方式有着很大的区别由于本人当前更多的从事于裸机开发所以这篇文章我会着重于将有关单片机裸机开发的阻塞与非阻塞那么废话不多说接下来我将与大家进行我的分享。裸机开发顾名思义是在一个“赤裸”的单片机中进行开发直接使用HAL库寄存器标准库开发方式操作单片机底层寄存器从而开启时钟配置GPIO配置各种片内外设使其互相配合成一个整体实现自己想要的业务操作。这些操作都是我们通过C语言编程然后编译器转换成机器码烧录到单片机的flash中的在这个过程中我们需要非常清楚单片机的运行逻辑玩过单片机的帅哥美女们都知道单片机的时钟是他的心脏所有单片机的操作都受时钟的指挥。只有时钟开启后单片机才可以工作要想深入理解可以参考STM32官方技术文档的时钟树文献每个单片机都有一个系统时钟和多个外设时钟系统时钟可以通过配置SysTick进行配置产生特定的系统时钟节拍也可以通过外设时钟操作定时器配置装载值和中断使能等实现定时器的定时操作。裸机开发还有个十分重要的特点就是他只在一个while循环里工作执行用户的业务程序。也就是说如果整个单片机项目程序是一个进程那么他只有一个线程。若是只有一个线程那么他的工作就有点要受限制了他只做一个任务且这个任务只按用户的安排去做。你可以想想你打游戏系统分配了很多任务如果你只做单一一个特定的任务那么是不是就很占用你的时间和资源了所以说裸机开发最最限制的不妨就是单片机运行的实时性了这是裸机开发的硬伤不同于实时操作系统你可以通过多任务多线程串行或并行处理一连串的任务只需要通过信号量和互斥锁等操作就可以实现多任务并行处理的效果了我将以自己业余时间里开发的基于STM32F103C8T6微处理器设计的简易心率检测仪项目给大家好好分享一下我在裸机开发中遇到的一些趣事。以简单按键扫描为例大多数单片机玩家可能会使用这种方式进行单片机按键输入的开发。代码int8_t key_scan(void) { int8_t key_value ; memset(key_value , 0 , sizeof(uint8_t)) ; is_key_exam_flag false ; // 按键检测标志位置0避免导致无效检测 if ((GPIOA-IDR(18) GPIOA-IDR(111) GPIOB-IDR(115) GPIOB-IDR(114) GPIOB-IDR(113) GPIOB-IDR(112)) true) { key_value PRESS_NO ; } // 没有按键按下 else { delay_ms(20) ; // 按键消抖处理 if (is_key_exam_flag) { if(!(GPIOA-IDR(18)) ) key_value PRESS_UP ; else if(!(GPIOA-IDR(111))) key_value PRESS_CLR ; else if(!(GPIOB-IDR(115))) key_value PRESS_ENT ; else if(!(GPIOB-IDR(114))) key_value PRESS_LEFT ; else if(!(GPIOB-IDR(113))) key_value PRESS_DOWN ; else if(!(GPIOB-IDR(112))) key_value PRESS_RIGHT ; } } // 按键按下 return key_value ; } /* 非阻塞消抖扫描普通方式 */这是一段很简单的按键扫描程序。我顺便和大家小提一嘴在单片机开发中按键按下后后产生一定的机械振动在那段期间电平读取不稳定所以一般需要延时20ms左右进行按键的消抖处理经过消抖处理后单片机输入寄存器(IDR)读取到的电平会相对稳定。在这个按键扫描程序中我使用了delay_ms(20)进行延时消抖这样可以有效读取到稳定的电平但是这样的延时是会空耗CPU单元效率的从而降低单片机的实时性我们把这种会空耗CPU效率的延时称为硬延时也就是系统级延时在这种延时下可能会对其他外设的工作造成影响20ms虽然微乎其微但是足以撼动一整个项目针对裸机开发实际上高手都会使用实时操作系统如果是大延时就更不用说了直接影响串口SPI等硬件通信导致数据交互异常就比如你打枪战你的网络突然来了个延迟掉帧你被对面爆头了这感觉想想都难受不仅错失了战机也导致了任务的失败所以在单片机开发里一定要减少硬延时的使用但是有些必要的硬延时还是要有的我曾经一步步走来吃了很多硬延时的亏导致我写的项目总是站着空耗耗尽了CPU的青春......针对这一现象或许聪明的小伙伴就思考了“诶既然你这么说那么我只让程序做延时不就好了吗还要系统延时还硬延时”既然硬的不行就来软的嘛恭喜你你想对了但是软延时真的能做到精确延时吗答案是不一定大家都知道时钟频率越快指令执行速度代码运行能力就越好那么为何不写个循环做计数处理延时呢事实上这样的延时是很不稳定的举个例子。for (int i 0 ; i 10000 ; i) ; // for int i 0 ; while (i 10000) i ; // while在上面代码中我们使用到了for和while循环在这种循环里单片机需要判断条件并循环执行循环中的语句虽然全都利用到了CPU没有造成他的空耗但是这样的延时未免太不追求稳定性了这是无法投入到正式使用中的你可以想象你正在做一盘菜给自己的另一半吃你真的敢完全凭自己的感觉不用靠计时器计时去把控一道菜的烹饪时间吗我想你也不敢吧一道好菜是注重烹饪时间的太早了入不了味太迟了又老了所以我们应该对时间有一定的概念不要太把自己交给时间上大学的朋友更是如此一定要抓紧时间学习要有自己独当一面的能力为此我将向大家隆重介绍一下单片机开发的好帮手单片机大大大管家单片机时间把握的严父定时器将军定时器属于单片机片内外设挂在PCLK时钟下具备定时中断溢出的功能有一定单片机基础的朋友都知道中断这一东西的好处它可以在达到一定中断条件后保留现场执行中断事件后在回来。定时器中断亦是如此前提是你要明白中断是什么我相信聪明的你多敲几遍代码就知道了【狗头】按键外设头文件#ifndef _KEY_H #define _KEY_H #include stdio.h #include stdint.h #include stdbool.h #include string.h #include stm32f10x.h #include delay.h #include timer3.h #define PRESS_NO 0x00 // 无按键按下 #define PRESS_CLR 0x01 // 返回键按下 #define PRESS_UP 0x02 // 上键按下 #define PRESS_ENT 0x03 // 进入键按下 #define PRESS_LEFT 0x04 // 左键按下 #define PRESS_DOWN 0x05 // 下键按下 #define PRESS_RIGHT 0x06 // 右键按下 // 按键赋值表 enum KEY_STATUS { KEY_READY 0, // 按键检测准备无输入 KEY_EXAM 1 // 按键输入检测输入中 } ; // 按键状态 extern bool is_key_init_flag ; // 按键初始化标志 void key_init(void) ; // 按键外设初始化 int8_t key_scan(void) ; // 按键扫描 bool print_key_value(int8_t *key_value) ; // 获得当前按键状态可调试用 #endif按键外设源文件#include key.h const char *key_str_buf [] { PRESS_NO , PRESS_CLR , PRESS_UP , PRESS_ENT , PRESS_LEFT , PRESS_DOWN , PRESS_RIGHT } ; // 按键字符串数组可调使用 const int8_t key_index_min 0 ; // 按键最小值 const int8_t key_index_max (int8_t)(sizeof(key_str_buf) / sizeof(key_str_buf[0])) - 1 ; // 按键最大值 bool is_key_init_flag false ; void key_init(void) { RCC-APB2ENR | 0x01 2 ; GPIOA-CRH 0xffff0ff0 ; GPIOA-CRH | 0x00003003 ; GPIOA-ODR 0xf6ff ; GPIOA-ODR | (18) | (111) ; // 初始化 PA11(KEY_CLR) PA8(KEY_UP) RCC-APB2ENR | 0x01 3 ; GPIOB-CRH 0x0000ffff ; GPIOB-CRH | 0x33330000 ; GPIOB-ODR 0x0fff ; GPIOB-ODR | (115) | (114) | (113) | (112) ; // 初始化 PB15(KEY_ENT) PB14(KEY_LEFT) PB13(KEY_DOWN) PB12(KEY_RIGHT) is_key_init_flag true ; } int8_t key_scan(void) { static int8_t key_status KEY_READY ; /* 按键实时状态 */ int8_t key_value ; memset(key_value , 0 , sizeof(uint8_t)) ; /* 按键输入数值 */ switch (key_status) { case KEY_READY : { if ((GPIOA-IDR(18) GPIOA-IDR(111) GPIOB-IDR(115) GPIOB-IDR(114) GPIOB-IDR(113) GPIOB-IDR(112)) true) { key_value PRESS_NO ; key_status KEY_READY ; } // 没有按键按下 else key_status KEY_EXAM ; break ; } case KEY_EXAM : { if (is_key_exam_flag) { if(!(GPIOA-IDR(18)) ) key_value PRESS_UP ; else if(!(GPIOA-IDR(111))) key_value PRESS_CLR ; else if(!(GPIOB-IDR(115))) key_value PRESS_ENT ; else if(!(GPIOB-IDR(114))) key_value PRESS_LEFT ; else if(!(GPIOB-IDR(113))) key_value PRESS_DOWN ; else if(!(GPIOB-IDR(112))) key_value PRESS_RIGHT ; key_status KEY_READY ; } else key_status KEY_READY ; break ; } default : key_status KEY_READY ; break ; } return key_value ; } /* 非阻塞消抖扫描状态机方式 */ bool print_key_value(int8_t *key_value) { if(key_value NULL) { printf(error key\r\n) ; return false ; } if(*key_value key_index_min || *key_value key_index_max) { printf(invalid key\r\n) ; return false ; } printf(KEY : %s\r\n , key_str_buf[*key_value]) ; return true ; }定时器头文件#ifndef _TIMER3_H #define _TIMER3_H #include stdbool.h #include stm32f10x.h #include sys.h extern bool is_timer3_init_flag ; // 定时器初始化标志位 extern bool is_key_exam_flag ; // 按键检测标志位(定时器非阻塞检测) extern uint16_t second_count ; // 定时器中断溢出计数(20ms自增1) void TIM3_Init(u16 arr,u16 psc) ; // 定时器初始化 bool clock_trigger(int interval_time_second) ; // 时钟触发函数 #endif定时器源文件#include timer3.h bool is_timer3_init_flag false ; bool is_key_exam_flag false ; uint16_t second_count 1 ; // 第1s时刻 void TIM3_Init(u16 arr,u16 psc) { RCC-APB1ENR|11; //TIM3时钟使能 TIM3-ARRarr; //设定计数器自动重装值 TIM3-PSCpsc; //预分频器设置 TIM3-DIER|10; //允许更新中断 TIM3-CR1|0x01; //使能定时器3 MY_NVIC_Init(0,3,TIM3_IRQn,2);//抢占0子优先级3组2 is_timer3_init_flag true ; } // 定时器初始化 void TIM3_IRQHandler(void) //TIM3中断 { if(TIM3-SR0X0001)//溢出中断 { is_key_exam_flag true ; // 20ms中断扫描一次按键输入 second_count ; // 20ms中断溢出自增1次 } TIM3-SR~(10);//清除中断标志位 } // 定时器中断服务函数 bool clock_trigger(int interval_time_second) { interval_time_second * 50 ; // 定时器3 20ms触发一次中断溢出故乘以50表示1s定时间隔 static bool trigger_flag false ; // 触发标志位 static bool refresh_flag true ; if (refresh_flag) { second_count 1 ; refresh_flag false ; } // 上电刷新定时器中断溢出计数仅1次刷新 if (second_count % (interval_time_second 1) 0) { trigger_flag true ; second_count 1 ; } else { trigger_flag false ; } // 1s定时检测,检测到边沿后触发时钟中断 return trigger_flag ; // 返回触发标志位 }主程序(无业务程序仅供参考)/****************************************** Designer : YiZhou Date : 2026/07/11 Brief : heart exam Version: V0.1 Special Thanks : ZDYZ *******************************************/ /* C语言库 */ #include stdio.h #include stdint.h #include stdbool.h #include string.h /* APP库 */ #include app.h #include app_data.h /* 系统硬件驱动库 */ #include sys.h #include stm32f10x.h #include delay.h #include timer3.h /* 硬件通信库 */ #include usart.h #include i2c1.h /* 外设库 */ #include key.h #include buzzer.h #include at24c02.h int8_t g_key_value ; // 全局按键 int main () { Stm32_Clock_Init(9) ; delay_init(72) ; TIM3_Init(199, 7199); // 20ms uart_init(72,115200) ; IIC_Init() ; key_init() ; buzzer_init() ; AT24CXX_Init() ; print_app_version() ; print_app_status_hardware() ; while(1) { // user_code } }我主要和大家分享一下我对于定时器开发的一种技巧使用以下是我的操作步骤1.编写定时器头文件声明定时器初始化和定时器中断函数声明一个外部定时器计数变量使用extern声明声明一些自己项目中需要的外设操作标志位这里我声明了一个按键操作标志位2.编写定时器源文件编写定时器初始化和定时器中断函数可以更具需要编写额外的业务函数定义定时器计数变量这种定时器技术变量可以全局使用3.在定时器中断函数里编写自己想要的实现中断溢出执行事件作为过来人提醒大家不管是定时器中断也好串口中断也罢在中断服务函数里尽量少写代码尽量控制一种外设的工作状态或者简单的变量递增计数不要把串口解析等非常占用时间的东西写在里面这样的中断是有风险的我不建议你们这么做通常也不会这样做。更关键的一点定时器是不占用CPU的但是定时器中断会抢占CPU这就是我们常说的抢占式处理逻辑了抢占式处理是具备优先级的优先级越高响应越快执行顺序越靠前。你可以大胆放心地使用总比你写软延时处理和系统硬延时处理要好的多至于效果如何你可以自己去尝试使用定时器定时读取数值或简单点定时串口输出内容就好你会发现不管你怎么按单片机总能快速响应并且输出很快如果你有一个由按键控制的蜂鸣器你会发现你一直按着按键他是一直响的中间没有明显隔断但是采用了硬延时你会明显察觉到那种“断层感”如果你想观察这种效果你可以打开一个定时器然后在while循环里让串口定时输出一个句子再在串口输出这条语句前系统硬延时2s你会发现定时器控制下的串口发送被系统硬延时强制延迟了2s但是2s一过就又正常定时发送了这很正常因为在系统节拍下(SysTick)系统硬延时的优先级是绝对最高的while (1) { delay_ms(1000) ; delay_ms(1000) ; if (clock_trigger()) { usart_send() ; } ... // 其他业务程序 }如果你喜欢裸机开发但是又不想空耗久等影响项目的运行效率你可以好好考虑学习一下定时器和中断并且好好参与这个单片机裸机程序的数据流设计和架构设计这样可以让你的单片机裸机程序也有一定的实时操作系统的效果但这两者肯定是没有可比性的单片机裸机开发不是潮流若是想要往高阶发展一定要学会实时操作系统拥有非阻塞思想这样会对于你日后的嵌入式开发十分有帮助若是往网络物联网AI方向发展更要学会linux下的pthread编程相信我孩子这会对你很有帮助linux操作系统是最适合做服务器开发的系统。总而言之。你一定要学会非阻塞编程思想他会让你更上一层楼字面意思上阻塞就会阻碍你的脚步了。