ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

单片机编程实例大全:从GPIO到状态机的100个实战代码解析

单片机编程实例大全:从GPIO到状态机的100个实战代码解析 1. 项目缘起与价值定位最近在整理资料时翻出了自己早年学习单片机时积攒的一大堆代码和笔记其中很多都是从一个简单的想法开始到最终调试成功的完整实例。这些实例从点亮一个LED到驱动一块复杂的TFT屏幕从读取一个按键到实现一个简易的PID控制器几乎覆盖了单片机入门到进阶的绝大多数场景。我意识到对于很多初学者甚至是有一定经验的开发者来说最缺的往往不是高深的理论而是这些“麻雀虽小五脏俱全”的、可以直接上手验证和修改的代码实例。它们就像乐高积木的零件单个看功能简单但组合起来就能构建出复杂的系统。因此我决定启动一个长期的分享计划将这些实例系统性地整理出来形成一个“单片机编程实例大全”。本篇是这个系列的第一部分涵盖了从第1例到第100例内容聚焦于51单片机如经典的STC89C52和STM32以STM32F103系列为代表的基础外设操作和核心编程思想。这个系列的价值在于“即查即用”和“举一反三”。每个实例都力求独立、完整包含清晰的硬件连接图或说明、完整的源代码以及关键代码的逐行注释。更重要的是我会在每个实例后面附上“编程要点”和“扩展思考”不仅告诉你代码怎么写更解释为什么这么写以及如何基于这个实例进行功能扩展或问题排查。例如一个简单的按键消抖程序我会对比软件延时消抖和状态机消抖的优劣并指出在实时性要求高的系统中该如何选择。我希望这份大全能成为你手边的“代码字典”和“灵感源泉”无论是为了完成课程设计、备战电子竞赛还是解决实际产品开发中的某个具体功能都能在这里找到可靠的参考和起点。2. 实例1-20 GPIO的“Hello World”与基础输入输出这一部分我们聚焦于单片机最基础也是最核心的功能通用输入输出GPIO。这是所有单片机程序的第一步理解GPIO的操作模式是后续一切复杂应用的基础。2.1 实例1-5LED的点亮、闪烁与流水灯实例1点亮一个LED这是所有单片机学习者的第一个程序。硬件上LED阴极通过限流电阻通常220Ω-1kΩ接地阳极连接单片机的一个GPIO引脚如P1.0。代码的核心就是将该引脚配置为推挽输出模式并输出高电平。// 51单片机示例 (Keil C51) #include reg52.h sbit LED P1^0; // 定义LED连接的引脚 void main() { LED 1; // 输出高电平LED亮 while(1); // 程序停在此处 }注意51单片机IO口上电默认为准双向口可以直接驱动LED。但对于STM32等单片机必须显式初始化GPIO模式推挽输出。实例2LED闪烁在实例1的基础上加入延时函数实现LED的周期性亮灭。// STM32 HAL库示例 (基于STM32CubeMX生成) #include “main.h” void SystemClock_Config(void); int main(void) { HAL_Init(); SystemClock_Config(); // GPIO初始化代码由CubeMX生成假设LED连接在PC13 while (1) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 翻转引脚电平 HAL_Delay(500); // 延时500毫秒 } }编程要点HAL_Delay()是一个阻塞延时在延时期间CPU无法执行其他任务。在简单的闪烁演示中可以使用但在实际项目中应避免长时间阻塞转而使用定时器中断。实例3-5流水灯通过循环移位操作控制多个LED依次点亮形成流水效果。这里的关键是理解位操作和数组的使用。// 51单片机使用P1口控制8个LED #include reg52.h #include intrins.h // 包含_crol_循环左移函数 void delay_ms(unsigned int ms) { /* 简易软件延时 */ } void main() { unsigned char led_pattern 0xFE; // 二进制11111110最低位LED亮 while(1) { P1 led_pattern; delay_ms(200); led_pattern _crol_(led_pattern, 1); // 循环左移一位 } }扩展思考如何实现双向流水、变速流水或根据按键控制流水方向这需要引入状态变量和更复杂的控制逻辑。2.2 实例6-10按键的检测、消抖与状态机实例6独立按键检测查询法读取连接在GPIO上的按键状态当检测到低电平时按键按下执行相应操作。// STM32示例按键接在PA0上拉电阻确保默认高电平 if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { // 检测到低电平按键按下 // 执行动作如翻转LED HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); }核心问题——按键抖动机械按键在闭合和断开的瞬间会产生数毫秒到数十毫秒的电平抖动直接读取会导致一次按下被误判为多次。因此必须消抖。实例7软件延时消抖在检测到按键状态变化后延时10-20ms再次检测如果状态稳定则确认按键动作。if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { // 初次检测到低电平 HAL_Delay(20); // 延时消抖 if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { // 再次确认 // 确认按键按下 // ...执行操作 } }缺点HAL_Delay()会阻塞整个程序影响系统实时性。实例8状态机消抖非阻塞式这是更优的解决方案。我们定义一个按键状态机通常包含IDLE空闲、DEBOUNCE消抖判断、PRESSED按下确认、RELEASE释放判断等状态。在主循环中定期如每1ms调用按键扫描函数根据当前状态和当前引脚电平进行状态转移。typedef enum { BTN_STATE_IDLE, BTN_STATE_DEBOUNCE, BTN_STATE_PRESSED, BTN_STATE_RELEASE } btn_state_t; btn_state_t key_state BTN_STATE_IDLE; uint32_t key_tick 0; void Key_Scan(void) { switch(key_state) { case BTN_STATE_IDLE: if(Read_Pin() PRESSED) { key_state BTN_STATE_DEBOUNCE; key_tick HAL_GetTick(); // 记录当前时间戳 } break; case BTN_STATE_DEBOUNCE: if(HAL_GetTick() - key_tick 20) { // 消抖时间到 if(Read_Pin() PRESSED) { key_state BTN_STATE_PRESSED; // **触发按键按下事件** On_Key_Pressed(); } else { key_state BTN_STATE_IDLE; // 抖动回到空闲 } } break; case BTN_STATE_PRESSED: if(Read_Pin() RELEASED) { key_state BTN_STATE_RELEASE; key_tick HAL_GetTick(); } break; case BTN_STATE_RELEASE: if(HAL_GetTick() - key_tick 20) { if(Read_Pin() RELEASED) { key_state BTN_STATE_IDLE; // **触发按键释放事件** On_Key_Released(); } else { key_state BTN_STATE_PRESSED; } } break; } } // 在主循环或定时器中断中定期调用Key_Scan()优势完全非阻塞消抖逻辑清晰易于扩展长按、连按等复杂功能。实例9-10矩阵键盘扫描当按键数量较多时为了节省IO口会使用矩阵键盘。扫描原理是将行线设置为输出列线设置为输入带上拉。依次将每一行输出低电平然后读取所有列线的状态。如果某列为低则说明该行该列的按键被按下。// 4x4矩阵键盘示例 uint8_t Key_Scan_Matrix(void) { uint8_t key_val 0xFF; // 循环扫描每一行 for(uint8_t row0; row4; row) { Set_Row_Low(row); // 当前行输出低电平 delay_us(10); // 短暂稳定时间 uint8_t col_data Read_Cols(); // 读取列数据 if(col_data ! 0x0F) { // 有列被拉低 // 根据row和col_data中低电平的位置计算键值 key_val Calculate_Key_Value(row, col_data); // 等待按键释放可结合状态机 while(Read_Cols() ! 0x0F); break; } Set_Row_High(row); // 恢复当前行为高电平 } return key_val; }避坑指南矩阵键盘扫描同样存在消抖问题通常将扫描得到的键值存入一个缓冲区并进行多次扫描确认或者将上述状态机思想应用于整个键盘扫描过程。3. 实例21-40 定时器/计数器的精准时空控制定时器是单片机的“心脏”它提供了精准的时间基准是实现延时、PWM、输入捕获、输出比较等高级功能的核心。3.1 实例21-25定时器中断实现精准延时与时钟实例21SysTick定时器实现毫秒延时STM32SysTick是Cortex-M内核的系统定时器常用于提供操作系统的心跳或简单的延时。HAL库的HAL_Delay()就是基于它实现的。我们可以自己封装一个更灵活的延时函数。volatile uint32_t sys_tick 0; // 全局滴答计数器 void SysTick_Handler(void) { // SysTick中断服务函数 sys_tick; } void delay_ms(uint32_t ms) { uint32_t start_tick sys_tick; while((sys_tick - start_tick) ms) { // 空循环等待 } }注意sys_tick在中断中被修改在主循环中被读取应声明为volatile防止编译器优化。实例22-23通用定时器中断实现多任务时间片假设我们需要三个任务分别以100ms、500ms和1s的周期执行。可以配置一个基本定时器如TIM2产生10ms的中断然后在中断服务程序中维护多个软件计数器。volatile uint32_t task1_counter 0 task2_counter 0 task3_counter 0; void TIM2_IRQHandler(void) { if(__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); // 每10ms进入一次 task1_counter; task2_counter; task3_counter; } } // 在主循环中检查 void main(void) { while(1) { if(task1_counter 10) { // 100ms到 task1_counter 0; Task1_Process(); // 执行任务1 } if(task2_counter 50) { // 500ms到 task2_counter 0; Task2_Process(); } // ... 任务3 } }关键点中断服务函数ISR要尽可能短小只做标记和计数复杂的处理放到主循环中。这被称为“前后台系统”。实例24-25定时器实现数码管动态扫描驱动多位共阴或共阳数码管必须使用动态扫描否则需要大量IO口。利用定时器中断来切换位选信号同时更新段选数据利用人眼视觉暂留形成稳定显示。uint8_t digit_buffer[4]; // 存储4位数码管要显示的数字码 uint8_t current_digit 0; // 当前扫描的位 void TIMx_IRQHandler(void) { // 定时器中断配置为2ms一次 // 1. 关闭所有位选消隐防止鬼影 DIGIT_OFF_ALL(); // 2. 设置段选数据为 digit_buffer[current_digit] SEG_SET(digit_buffer[current_digit]); // 3. 打开当前位的位选 DIGIT_ON(current_digit); // 4. 指向下一位 current_digit (current_digit 1) % 4; }避坑经验“鬼影”是动态扫描的常见问题原因是段选数据变化时位选信号还未完全关闭导致短暂显示错误。解决方法是在切换位选前先关闭所有位选消隐设置好段选数据后再打开对应的位选。这个消隐步骤至关重要。3.2 实例26-35PWM输出与输入捕获的应用实例26-30PWM驱动LED呼吸灯与舵机PWM脉冲宽度调制通过调节占空比来控制平均电压或电流。呼吸灯改变PWM的占空比使LED亮度平滑变化。通常使用定时器的PWM输出模式。// STM32 HAL库配置PWM输出以TIM1 CH1为例 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); // 启动PWM uint16_t duty 0; int8_t step 5; while(1) { duty step; if(duty 1000 || duty 0) step -step; // 到达边界后反向 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, duty); // 更新比较值改变占空比 HAL_Delay(10); }舵机控制舵机需要周期为20ms50Hz脉宽在0.5ms到2.5ms之间的PWM信号。脉宽对应舵机角度如1.5ms对应90度。计算比较值公式为Compare (PulseWidth / Period) * (ARR 1)。假设定时器时钟72MHz预分频72-1则计数频率为1MHz周期20ms对应ARR19999。那么1.5ms脉宽对应的比较值就是(0.0015 / 0.020) * 20000 1500。实例31-35输入捕获测量脉冲宽度与频率输入捕获功能可以精准测量外部脉冲的高电平宽度、低电平宽度或周期。例如测量红外遥控信号的脉宽或测量旋转编码器的转速。// 思路配置定时器为输入捕获模式上升沿触发。 // 第一次上升沿中断记录计数器值T1。 // 设置为下降沿触发。 // 下降沿中断记录计数器值T2。则高电平时间 (T2 - T1) * 定时器计数周期。 // 再设置为上升沿触发准备下一次捕获。 // 注意处理计数器溢出的情况。一个实用的测量频率的简易方法如果不需要极高精度可以使用“测周法”或“测频法”。测周法测量一个完整周期的时间T则频率f1/T。适用于低频信号。测频法在固定闸门时间如1秒内计数脉冲的个数N则频率fN/1。适用于高频信号。输入捕获模式通常用于测周法。4. 实例36-60 串口通信与数据交互串口UART是单片机与外界PC、传感器、另一片单片机通信最常用的方式之一。4.1 实例36-45轮询、中断与DMA三种收发模式实例36-38轮询方式发送与接收轮询是最简单的方式但会阻塞CPU。// 发送一个字符 void UART_SendChar(uint8_t ch) { while(!(USART1-SR USART_SR_TXE)); // 等待发送数据寄存器空 USART1-DR ch; // 写入数据 } // 接收一个字符阻塞等待 uint8_t UART_ReceiveChar(void) { while(!(USART1-SR USART_SR_RXNE)); // 等待接收到数据 return (uint8_t)USART1-DR; }缺点UART_ReceiveChar()会一直死等直到收到数据。这在有实时性要求的系统中不可接受。实例39-42中断方式接收不定长数据这是最常用的方式。开启接收中断每收到一个字节就进入中断服务程序将数据存入缓冲区如环形队列。#define RX_BUF_SIZE 128 uint8_t uart_rx_buf[RX_BUF_SIZE]; uint16_t uart_rx_write_idx 0; uint16_t uart_rx_read_idx 0; void USART1_IRQHandler(void) { if(USART1-SR USART_SR_RXNE) { uint8_t data USART1-DR; // 读取数据清除标志 // 简单的线性缓冲区实际应用建议用环形缓冲区 if(uart_rx_write_idx RX_BUF_SIZE) { uart_rx_buf[uart_rx_write_idx] data; } } } // 在主循环中处理接收到的数据包 void Process_UART_Data(void) { if(uart_rx_read_idx uart_rx_write_idx) { uint8_t cmd uart_rx_buf[uart_rx_read_idx]; // ... 根据cmd解析数据包 uart_rx_read_idx; // 简单处理处理完所有数据后重置索引 if(uart_rx_read_idx uart_rx_write_idx) { uart_rx_read_idx uart_rx_write_idx 0; } } }关键改进——环形缓冲区上述线性缓冲区在读写指针追上时会丢失数据。环形缓冲区通过取模运算实现首尾相接是更可靠的数据结构。实例43-45DMA方式实现高速、后台数据收发DMA直接存储器访问可以在不占用CPU的情况下在外设和内存之间搬运数据。对于大量、高速的串口数据传输如固件升级、图像传输至关重要。发送配置DMA从内存数组搬运数据到USART的发送数据寄存器TDR。启动后CPU可以去做其他事情DMA会在发送完成后产生中断通知CPU。接收配置DMA从USART的接收数据寄存器RDR搬运数据到内存数组。可以配合串口的空闲中断IDLE来检测一帧数据接收完成。当总线空闲一段时间后产生空闲中断此时DMA已将所有数据搬运到缓冲区CPU在中断中处理数据并重新配置DMA。4.2 实例46-60通信协议解析与实战实例46-50自定义简单文本协议例如定义协议格式为CMDPARAM1PARAM2\n如SETLEDON\n。在中断接收缓冲区中需要寻找帧头如特定字符$、帧尾如换行符\n或根据长度来分割数据包然后解析逗号分隔的字段。void Parse_Command(uint8_t *buf, uint16_t len) { // 1. 检查帧头帧尾 if(buf[0] ! $ || buf[len-1] ! \n) return; // 2. 提取命令字段简易版使用strtok需注意线程安全 char *token strtok((char*)buf1, “”); // 跳过帧头 if(token ! NULL) { if(strcmp(token, “SET”) 0) { token strtok(NULL, “”); // ... 解析参数并执行 } else if(strcmp(token, “GET”) 0) { // ... } } }实例51-55解析Modbus RTU协议Modbus是工业领域广泛应用的协议。以读取保持寄存器功能码03为例主机发送[从机地址][03][起始寄存器地址高][低][寄存器数量高][低][CRC低][CRC高]。从机回复[从机地址][03][字节数][数据1高][数据1低]...[数据N高][数据N低][CRC低][CRC高]。 实现要点字节超时管理使用一个定时器在收到每个字节时重置。如果超过3.5个字符时间没有新字节则认为一帧结束。CRC校验对整帧数据除CRC部分外进行CRC-16计算并与接收到的CRC比对确保数据正确。协议处理根据功能码执行相应的操作读线圈、读寄存器、写单个寄存器等并组织响应帧。实例56-60模拟AT指令交互很多Wi-Fi如ESP8266、蓝牙、GSM模块使用AT指令。交互模式通常是“发送指令 - 等待模块回复 - 解析回复”。bool Send_AT_Command(const char *cmd, const char *expected_resp, uint32_t timeout) { UART_SendString(cmd); // 发送指令如“AT\r\n” uint32_t start HAL_GetTick(); clear_rx_buffer(); // 清空接收缓冲区 while(HAL_GetTick() - start timeout) { if(UART_Receive_Line(rx_buf)) { // 接收到一行数据 if(strstr(rx_buf, expected_resp) ! NULL) { // 找到期望的响应 return true; } else if(strstr(rx_buf, “ERROR”) ! NULL) { // 收到错误 return false; } } } return false; // 超时 }避坑经验务必添加超时机制防止因模块无响应导致程序卡死。注意指令的结尾符通常是\r\n但有些模块可能是\n或\r。模块返回的数据可能包含多余的空格、换行解析时要做修剪trim处理。对于复杂的多行响应如ATCWLAP列出Wi-Fi列表需要设计状态机来逐行解析直到遇到“OK”或“ERROR”。5. 实例61-80 常用传感器与执行器驱动这一部分我们将接触具体的传感器和执行器将前面学到的GPIO、定时器、串口等知识综合运用起来。5.1 实例61-70数字与模拟传感器实例61-63温湿度传感器DHT11DHT11是单总线数字传感器。通信过程包括主机拉低总线至少18ms起始信号- 释放总线等待20-40us - 传感器响应80us低电平 - 随后80us高电平 - 开始传输40位数据温湿度整数小数、校验和。每一位数据以50us低电平开始高电平的持续时间决定数据位是026-28us还是170us。// 读取一个bit的伪代码 uint8_t DHT11_ReadBit(void) { while(DHT11_PIN_IS_LOW); // 等待50us低电平开始位结束 delay_us(40); // 等待到高电平的中间时刻 if(DHT11_PIN_IS_HIGH) { delay_us(80); // 等待高电平结束如果是1 return 1; } else { return 0; } }要点单总线通信对时序要求极其严格必须关闭中断或确保中断服务程序非常短否则容易读错数据。校验和是前4个字节湿度整数、湿度小数、温度整数、温度小数相加的低8位应与第5个字节校验和相等。实例64-67超声波测距模块HC-SR04驱动流程触发引脚Trig给出一个至少10us的高脉冲 - 模块自动发出8个40kHz超声波 - 回声引脚Echo输出高电平高电平持续时间与距离成正比。距离 (高电平时间 * 声速) / 2。// 使用输入捕获测量高电平时间 HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); delay_us(12); HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); // 等待Echo引脚变高并启动输入捕获定时器 // 在Echo下降沿中断中获取捕获值计算时间差 float distance_cm (pulse_width_us * 0.0343) / 2.0; // 声速按343m/s计算注意事项测量距离有盲区通常2-3cm超过最大量程通常4-5米可能无法收到回波。多次测量取平均可以提高稳定性。实例68-70ADC读取模拟量电位器、光敏电阻以STM32的ADC读取电位器电压为例。// 1. 启动ADC转换 HAL_ADC_Start(hadc1); // 2. 等待转换完成 if(HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { // 3. 获取转换结果 uint16_t adc_value HAL_ADC_GetValue(hadc1); // 4. 转换为电压值 (假设12位ADC参考电压3.3V) float voltage (adc_value / 4095.0) * 3.3; }扩展应用光敏电阻通过分压电路接入ADCADC值反映了环境光强。可以据此实现自动调光LED。热电偶、压力传感器等模拟输出传感器也通过ADC读取。5.2 实例71-80执行器控制进阶实例71-75步进电机驱动ULN2003/DRV8825步进电机控制的核心是脉冲和方向。四相五线步进电机ULN2003驱动采用单极驱动按一定顺序给线圈通电如A-AB-B-BC-C-CD-D-DA实现步进。速度由脉冲间隔控制。const uint8_t step_seq[4] {0x09 0x0A 0x06 0x05}; // A B C D 线圈通电顺序 for(int i0; isteps; i) { set_coils(step_seq[current_step]); current_step (current_step direction) % 4; delay_ms(step_delay); // 控制速度 }两相四线步进电机DRV8825驱动采用双极驱动需要PWM和方向信号。DRV8825驱动器接收脉冲STEP和方向DIR信号。每个脉冲走一步。细分设置通过驱动器上的拨码开关配置。实例76-80直流电机控制与H桥原理直流电机通过电压大小和方向控制转速和转向。常用H桥电路实现。H桥由四个开关管MOSFET组成通过对角线导通来控制电流方向。// 控制逻辑假设IN1 IN2控制一个H桥 // 正转 IN1 1; IN2 0; // 反转 IN1 0; IN2 1; // 刹车快速停止 IN1 1; IN2 1; // 或 IN1 0; IN2 0; 取决于具体电路 // 停止惯性滑行 IN1 0; IN2 0;PWM调速将上述IN1或IN2的信号改为PWM波即可调节平均电压实现调速。例如正转调速IN1 PWM; IN2 0;。反转调速IN1 0; IN2 PWM;。重要安全提示H桥的同一侧上下管如IN1和IN2绝对不能同时导通否则会造成电源短路瞬间烧毁MOSFET在实际驱动代码中必须在改变方向时插入一个短暂的死区时间确保一侧完全关闭后再开启另一侧。许多集成的电机驱动芯片如L298N TB6612已经内置了死区保护和硬件逻辑使用起来更安全简单。6. 实例81-100 综合项目与系统设计思想最后这20个实例我们将尝试把多个模块组合起来完成一些小型的综合项目并引入一些简单的系统设计思想。6.1 实例81-90数据采集与显示系统项目构想制作一个环境监测仪实时采集温度、湿度、光照强度并显示在OLED屏幕上同时通过串口将数据发送到上位机。实例81-83多传感器数据融合采集任务调度使用一个定时器产生1s的中断作为系统心跳。在中断中设置标志位在主循环中检查标志位每1秒执行一次传感器数据采集任务DHT11 ADC读取光敏。非阻塞设计DHT11的读取需要几十毫秒如果使用阻塞延时会严重影响系统响应。可以将其拆分为多个状态START-WAIT_RESPONSE-READ_BITS-PROCESS_DATA。在主循环的状态机中逐步执行每步之间用极短的延时或等待引脚状态变化这样就不会长时间阻塞。实例84-87OLED显示驱动I2C/SPI以0.96寸SSD1306 OLEDI2C接口为例。初始化发送一系列初始化命令关闭显示、设置对比度、扫描方向、起始行等。显存OLED内部有一个GDDRAM图形显示数据RAM对应屏幕的每个像素点128x64。我们操作的是一个在MCU内存中建立的屏幕缓冲区uint8_t buffer[1024]每个字节对应垂直的8个像素点。画点函数是基础。void OLED_DrawPoint(uint8_t x uint8_t y uint8_t mode) { if(x128 || y64) return; uint16_t page y / 8; uint16_t bit y % 8; if(mode) buffer[x page * 128] | (1 bit); // 置1点亮 else buffer[x page * 128] ~(1 bit); // 清0熄灭 }显示函数将整个缓冲区通过I2C一次性发送到OLED的GDDRAM。I2C传输需要先发送控制字节0x40表示后续是数据然后连续发送1024字节的缓冲区数据。显示内容基于画点函数可以构建画线、画矩形、显示字符、显示字符串、显示图片取模等高级函数。实例88-90上位机通信与数据可视化在PC端可以使用Python的pyserial库读取串口数据并用matplotlib或PyQtGraph进行实时绘图。# Python 示例代码片段 import serial import time ser serial.Serial(COM3, 9600, timeout1) while True: if ser.in_waiting: line ser.readline().decode(utf-8).strip() if line: # 假设数据格式为 “T:25.6,H:60.5,L:320” data parse_line(line) # 自定义解析函数 update_plot(data) # 更新图表单片机端则需要按照约定好的格式如JSON、CSV或自定义文本协议定时发送数据。6.2 实例91-100状态机与事件驱动编程当系统功能变得复杂有多个输入按键、串口命令、传感器阈值和多个输出LED、屏幕、电机时简单的if-else或switch-case会变得难以维护。这时需要引入状态机FSM和事件驱动的思想。实例91-95基于状态机的智能风扇控制器假设风扇有三种模式关闭OFF、低速LOW、高速HIGH。有一个按键用于切换模式一个温度传感器用于自动控制。定义状态OFF_STATELOW_STATEHIGH_STATE。定义事件KEY_PRESS_EVENTTEMP_HIGH_EVENT温度30℃TEMP_LOW_EVENT温度25℃。状态转移表当前状态事件动作下一状态OFFKEY_PRESS开启低速风扇LOWOFFTEMP_HIGH开启高速风扇HIGHLOWKEY_PRESS切换到高速HIGHLOWTEMP_LOW关闭风扇OFFHIGHKEY_PRESS关闭风扇OFFHIGHTEMP_LOW切换到低速LOW程序实现主循环中不断检测事件按键扫描、温度读取根据当前状态和发生的事件查表执行动作并迁移到下一个状态。实例96-100简易事件驱动框架更进一步我们可以实现一个简单的事件队列。typedef enum { EV_NONE EV_KEY_SHORT_PRESS EV_KEY_LONG_PRESS EV_UART_RX_PACKET EV_TIMER_1S // ... 更多事件 } event_t; #define EVENT_QUEUE_SIZE 10 event_t event_queue[EVENT_QUEUE_SIZE]; uint8_t ev_write_idx 0 ev_read_idx 0; void Post_Event(event_t ev) { // 将事件放入队列注意队列满的处理 event_queue[ev_write_idx] ev; ev_write_idx (ev_write_idx 1) % EVENT_QUEUE_SIZE; } event_t Get_Event(void) { if(ev_read_idx ! ev_write_idx) { event_t ev event_queue[ev_read_idx]; ev_read_idx (ev_read_idx 1) % EVENT_QUEUE_SIZE; return ev; } return EV_NONE; } void main(void) { // 初始化... while(1) { event_t ev Get_Event(); switch(ev) { case EV_KEY_SHORT_PRESS: // 处理短按 break; case EV_UART_RX_PACKET: // 处理串口数据包 break; case EV_TIMER_1S: // 执行1秒任务 break; default: // 无事件可进入低功耗模式 break; } } } // 在中断服务程序按键、串口、定时器中只调用Post_Event不做复杂处理。这种架构将事件的产生和处理解耦使得主程序逻辑清晰易于扩展和维护是开发复杂嵌入式应用的基石。通过这100个从易到难、从点到面的实例我希望构建的不仅仅是一个代码仓库更是一条清晰的学习路径和一套解决问题的思维工具。每个实例都像一颗螺丝钉单独看很简单但当你掌握了它们并学会了如何将它们有机组合你就拥有了构建任何复杂嵌入式系统骨架的能力。在后续的系列中我们将深入更专题化的领域如低功耗设计、RTOS应用、文件系统、图形界面等继续拆解更多的“积木零件”。
返回列表