1. 从电赛视角重新理解单片机为什么“速通”是可行的很多人第一次接触电赛看到题目里要求“测量某信号”“控制某电机”“显示某波形”第一反应是去翻教材从第一章的二进制开始啃。这个路径不是不行但时间成本极高。我带过几届校赛队伍也见过不少零基础的同学在两周内把单片机的常用外设跑通、最后拿到省奖的案例。核心原因在于电赛对单片机的考察是“应用级”的不是“原理级”的。你不需要会设计CPU但你必须知道GPIO怎么配、PWM怎么出、ADC怎么采、中断怎么进。标题里列出的这一串——GPIO、PWM、ADC、TIM、IRQ、I2C、SPI、UART、WDG、DMA——基本就是电赛控制类、信号类题目会用到的全部外设集合。它们不是孤立的而是围绕“采集—处理—输出”这条主线串起来的。比如2023年电赛的某道控制题本质就是ADC采传感器电压TIM定时触发采样IRQ处理数据PWM驱动执行器UART上报数据。你把这条链路打通题目换汤不换药。所以“一个视频学完单片机”这个说法我的理解是一个视频的时长足够覆盖这些外设的最小可用集。不是学完所有寄存器而是学完“能跑起来”的那部分。这篇文章就按这个思路把每个外设的核心用法、配置逻辑、踩坑点讲清楚让你看完能直接上手改代码。提示本文默认你用的是STM32F103C8T6或同级别芯片HAL库开发。如果你用的是51单片机或STC系列逻辑相通但寄存器操作会更多我会在关键处标注差异。2. 核心外设逐个拆解从“能跑”到“跑稳”2.1 GPIO八种模式到底怎么选GPIO是单片机的“手脚”但很多人卡在模式选择上。网上搜“gpio的8种工作模式”能出来一堆表格但实际电赛里常用的就四种推挽输出、开漏输出、上拉输入、浮空输入。推挽输出用于驱动LED、继电器这类需要强驱动能力的负载。开漏输出用于I2C总线因为需要线与逻辑。上拉输入用于按键浮空输入用于ADC采样引脚但ADC引脚通常配模拟输入模式这个后面说。我见过一个典型错误用推挽输出直接驱动I2C的SDA线结果两个设备同时拉低时电流倒灌芯片发烫。正确做法是开漏输出加外部上拉电阻。这个坑在电赛现场很致命因为I2C通信失败往往表现为“读不到数据”新手会以为是地址错了其实是电气特性不对。配置代码用HAL库很简单GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_5; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);注意STM32F103的PA13、PA14默认是SWD调试口如果你把它们配成普通GPIO下载一次程序后就再也连不上了。解决办法是按住复位键点下载或者用STM32CubeProgrammer改选项字节。这个坑我踩过当时以为板子坏了。2.2 PWM不只是调占空比PWM在电赛里出现频率极高舵机控制、电机调速、RGB调光、DAC模拟输出。但很多人只会用__HAL_TIM_SET_COMPARE改占空比不知道频率怎么算。以STM32F103为例TIM1挂在APB2上时钟72MHz。如果你要出50Hz的舵机PWM预分频器PSC和自动重装载值ARR这样算目标频率 72MHz / ((PSC1) * (ARR1))取PSC71则计数频率1MHz取ARR19999则周期20ms即50Hz代码htim1.Instance TIM1; htim1.Init.Prescaler 71; htim1.Init.Period 19999; htim1.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(htim1); TIM_OC_InitTypeDef sConfigOC {0}; sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 1500; // 1.5ms脉宽舵机中位 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1);提示舵机的Pulse值对应0.5ms到2.5ms对应0度到180度。但不同品牌舵机有差异建议先用示波器看实际输出或者用“慢扫”方式找边界。PWM还有一个高级用法单脉冲模式。电赛里有时需要发一个精确宽度的脉冲比如触发某传感器。用TIM的单脉冲模式配置好ARR和Pulse触发一次就自动停止比用延时函数精准得多。2.3 ADC采样周期和滤波是两回事ADC是电赛信号类的核心。很多人采出来的数据跳得厉害第一反应是“ADC不准”其实是采样周期没配对。STM32F103的ADC时钟最高14MHz采样周期可选1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个周期。总转换时间 采样周期 12.5个周期。如果你要采一个高阻抗信号源采样周期太短会导致采样电容充不满读数偏低。我的经验是默认用55.5或71.5周期除非你确定信号源阻抗很低。代码ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_71CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig);采到数据后滤波是另一回事。电赛里常用的滤波有均值滤波、中值滤波、一阶低通滤波。均值滤波适合消除随机噪声中值滤波适合消除脉冲干扰一阶低通适合跟踪缓慢变化的信号。一阶低通滤波的C语言实现float alpha 0.1f; // 滤波系数越小越平滑但滞后越大 float filtered 0.0f; filtered alpha * new_value (1 - alpha) * filtered;注意alpha的选择要看信号频率。如果你采的是50Hz工频信号采样率1kHzalpha取0.1左右比较合适。如果信号本身变化很快alpha要调大否则会把有用信号滤掉。还有一个坑ADC的参考电压。STM32F103的VREF默认接VDDA如果你用USB供电VDDA可能只有4.7V左右不是精确的5V。电赛里如果要求测量精度最好用外部基准源比如REF3033。没有基准源的话至少用万用表量一下VDDA实际值在代码里做校准。2.4 TIM不只是延时TIM在电赛里承担三个角色精确定时、PWM输出、输入捕获。精确定时用HAL_TIM_Base_Start_IT然后在中断里做事情。输入捕获用来测频率、测脉宽比如超声波测距的回波信号。输入捕获的配置稍微复杂核心是设置捕获边沿和预分频。以测频率为例TIM_IC_InitTypeDef sConfigIC {0}; sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0; HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1);在中断回调里记录两次上升沿之间的计数值乘以计数周期就是周期取倒数就是频率。提示测高频信号时如果频率超过定时器时钟的一半会溢出。STM32F103的TIM2挂在APB1上时钟72MHz理论上能测36MHz但实际受中断响应时间限制建议不超过1MHz。再高就用外部计数器或FPGA。2.5 IRQ中断优先级是门学问中断IRQ是单片机的“神经反射”。电赛里常见的中断源定时器溢出、ADC转换完成、UART接收、外部按键。中断优先级配置不对会出现“串口数据丢了”“按键不响应”这类问题。STM32的NVIC优先级分抢占优先级和子优先级。抢占优先级高的可以打断低的子优先级只在同时挂起时决定谁先执行。我的建议是串口接收中断抢占优先级1定时器中断抢占优先级2ADC中断抢占优先级3按键中断抢占优先级4这样串口数据不会因为定时器中断而丢失。配置代码HAL_NVIC_SetPriority(USART1_IRQn, 1, 0); HAL_NVIC_EnableIRQ(USART1_IRQn);注意中断服务函数里不要做耗时操作。我见过有人在串口中断里做浮点运算和字符串拼接结果主循环卡死。正确做法是中断里只把数据存到缓冲区置个标志位主循环里处理。2.6 I2C时序和上拉电阻I2C在电赛里常用于连接EEPROM、OLED、传感器。HAL库的I2C函数用起来简单但硬件I2C容易卡死这是STM32F103的已知问题。我的建议是能用软件I2C就用软件I2C或者用HAL_I2C_Mem_Read/Write的超时版本。软件I2C的核心是延时。延时太短时序不满足延时太长速度慢。对于100kHz的I2C半周期约5us。用__NOP()循环实现void I2C_Delay(void) { for (volatile int i 0; i 10; i); }上拉电阻选4.7kΩ到10kΩ。电阻太小功耗大电阻太大上升沿变缓高速通信会出错。电赛里如果I2C设备多总线电容大上拉电阻要适当减小。2.7 SPI速度与片选SPI比I2C快常用于驱动TFT屏、Flash、高速ADC。SPI的四个信号SCK、MOSI、MISO、CS。CS片选是关键很多新手忘了拉低CS导致通信失败。SPI的配置重点是时钟极性和相位CPOL和CPHA。这四个组合对应四种模式必须和从设备手册一致。比如某款ADC用模式1CPOL0CPHA1你配成模式0就读不到数据。hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_2EDGE;提示SPI的CS可以用软件控制也可以用硬件NSS。软件控制更灵活但要注意在传输前后手动拉低拉高。2.8 UART电赛的“生命线”UART是电赛里最重要的外设之一因为它是和上位机、蓝牙、WiFi模块通信的主要方式。电赛现场经常需要把数据传到电脑上画波形或者用手机APP控制。UART的配置重点是波特率。常见115200但如果你用蓝牙模块可能只支持9600。配置不对就是乱码。huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; HAL_UART_Init(huart1);接收用中断或DMA。中断方式适合不定长数据DMA方式适合大量数据。电赛里我常用空闲中断DMA一帧数据接收完触发空闲中断效率高。2.9 WDG看门狗是保命符看门狗WDG在电赛里容易被忽略但现场环境复杂电源波动、静电干扰都可能导致程序跑飞。独立看门狗IWDG用内部低速时钟即使主时钟挂了也能复位。配置IWDGhiwdg.Instance IWDG; hiwdg.Init.Prescaler IWDG_PRESCALER_64; hiwdg.Init.Reload 4095; // 约1秒超时 HAL_IWDG_Init(hiwdg);然后在主循环里定期喂狗HAL_IWDG_Refresh(hiwdg);注意喂狗间隔要小于超时时间。如果你在中断里喂狗主循环卡死时中断还在跑看门狗就失效了。正确做法是在主循环里喂狗。2.10 DMA解放CPUDMA在电赛里用于ADC多通道采集、UART大量数据传输、SPI刷屏。没有DMACPU要一直等着数据搬完效率低。ADCDMA的配置HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, BUFFER_SIZE);这样ADC采完一个通道自动存到数组里采完一轮触发中断。电赛里做FFT分析时用DMA采1024个点CPU同时做其他事情。提示DMA的缓冲区要用__attribute__((aligned(4)))对齐否则可能出错。这个坑我在某次电赛中踩过数据偶尔错位查了半天。3. 电赛实战从外设到系统3.1 典型题目拆解信号测量与显示假设题目要求测量一个0-3.3V的直流信号用OLED显示电压值同时通过UART上传到电脑。系统链路ADC采样 - 滤波 - 计算电压 - OLED显示 - UART发送。ADC用DMA采100个点存到数组。主循环里做均值滤波乘以3.3/4096得到电压。OLED用I2C驱动UART用DMA发送。关键点ADC的参考电压要准最好用万用表量VDDAOLED刷新率不要太高否则I2C占用太多时间UART发送用DMA避免阻塞主循环3.2 典型题目拆解电机控制题目要求用PWM控制直流电机转速用编码器测速用PID闭环。系统链路TIM PWM输出 - 电机驱动 - 编码器 - TIM输入捕获 - PID计算 - 调整PWM。编码器用TIM的编码器模式直接读计数。PID用位置式或增量式电赛里增量式更常用因为不需要积分限幅。int PID_Incremental(int error) { static int last_error 0, prev_error 0; int delta Kp * (error - last_error) Ki * error Kd * (error - 2*last_error prev_error); prev_error last_error; last_error error; return delta; }注意PID参数要现场调。先调Kp让系统有响应但不过冲再加Ki消除静差最后加Kd抑制振荡。3.3 典型题目拆解无线通信题目要求两块单片机之间无线传输数据。方案UART 无线模块。一块单片机采集数据通过UART发给无线模块另一块接收。关键点无线模块的波特率要和单片机一致数据包要加帧头和校验防止误码接收端用空闲中断DMA避免丢包4. 常见问题与排查技巧实录4.1 程序下载后不运行现象下载成功但板子没反应。排查检查BOOT0和BOOT1引脚必须都接地检查复位电路复位键是否卡住检查晶振用示波器看有没有起振检查电源3.3V是否正常我的经验有一次是晶振旁边的负载电容焊错了换成20pF就好了。4.2 ADC读数跳动大现象采固定电压读数在±50以内跳。排查采样周期是否太短改成239.5周期试试参考电压是否稳定用示波器看VDDA模拟地和数字地是否单点接地信号源阻抗是否太高加电压跟随器我的经验电赛现场电源噪声大ADC的VDDA和VSSA之间并一个10uF和0.1uF电容效果明显。4.3 I2C通信失败现象HAL_I2C_Master_Transmit返回HAL_ERROR。排查上拉电阻是否接了4.7kΩ从设备地址是否正确注意7位地址要左移一位时序是否满足用逻辑分析仪抓波形是否卡在BUSY状态需要重新初始化I2C我的经验STM32F103的硬件I2C有BUG如果卡死用HAL_I2C_DeInit再HAL_I2C_Init能恢复。但最好用软件I2C。4.4 串口乱码现象电脑收到乱码。排查波特率是否一致115200对115200晶振频率是否正确8MHz对8MHz数据位、停止位、校验位是否匹配地线是否共地我的经验有一次是USB转串口模块的晶振是12MHz但驱动里选的是8MHz导致波特率偏差。换模块就好了。4.5 看门狗导致频繁复位现象程序跑一会儿就复位。排查喂狗间隔是否小于超时时间是否有长时间阻塞的操作比如延时是否在中断里喂狗我的经验把喂狗放在主循环开头确保每次循环都喂。如果某个任务耗时超过超时时间把看门狗超时调大。5. 工具链与开发环境少走弯路5.1 开发工具选择IDESTM32CubeIDE免费集成CubeMX和编译调试配置工具STM32CubeMX图形化配置引脚和外设下载器ST-Link V2便宜好用调试器ST-Link配合IDE的调试功能能看变量、设断点5.2 代码组织建议电赛代码要模块化每个外设一个.c和.h文件。主循环里用状态机避免阻塞。while (1) { switch (state) { case STATE_IDLE: if (start_flag) state STATE_SAMPLE; break; case STATE_SAMPLE: // 采样 state STATE_PROCESS; break; case STATE_PROCESS: // 处理 state STATE_OUTPUT; break; case STATE_OUTPUT: // 输出 state STATE_IDLE; break; } HAL_IWDG_Refresh(hiwdg); }5.3 版本管理电赛现场改代码频繁建议用Git做本地版本管理。每次改完提交一次出问题能回滚。git init git add . git commit -m ADCDMA可用版本6. 从零基础到电赛获奖我的学习路径建议如果你是完全零基础我建议按这个顺序第一天GPIO输出点亮LED学会用CubeMX配引脚第二天GPIO输入按键控制LED理解上拉和浮空第三天UART和电脑通信打印“Hello”第四天TIM定时中断1秒翻转LED第五天PWM控制LED亮度理解占空比第六天ADC采电位器电压UART打印第七天I2C驱动OLED显示电压第八天SPI驱动TFT屏第九天DMAADC多通道采集第十天综合项目信号测量与显示这个路径的关键是每天都有可见的成果。不要一开始就啃寄存器手册先用HAL库跑起来再回头看原理。提示电赛前一个月找历年题目做模拟。限时4天3夜完整走一遍流程。这个训练比看视频有用得多。7. 一些不写在教材里的经验关于示波器电赛现场一定要带示波器。PWM波形、UART波形、I2C波形看一眼就知道问题在哪。没有示波器用逻辑分析仪也行但逻辑分析仪只能看数字信号。关于电源电赛现场电源质量参差不齐。带一个线性稳压电源或者至少带几个大电容。电机启动时电流冲击大可能导致单片机复位。关于代码备份U盘、网盘、邮箱多处备份。我见过有人电脑蓝屏代码全丢现场重写。关于队友分工一个人写代码一个人调硬件一个人写报告。不要三个人都写代码也不要三个人都调硬件。关于时间分配第一天确定方案第二天搭硬件第三天写代码第四天调试和写报告。不要前两天都在纠结方案。关于心态电赛现场一定会遇到问题。遇到问题不要慌先看现象再查资料再问队友。实在不行换方案。能跑起来的简单方案比跑不起来的复杂方案强。最后分享一个我常用的调试技巧用LED做状态指示。程序跑到哪一步LED就亮哪个。比串口打印快而且不占用UART。比如LED1_ON(); // 进入ADC采样 // ... ADC代码 LED1_OFF(); LED2_ON(); // 进入数据处理 // ... 处理代码 LED2_OFF();这样一眼就能看出程序卡在哪。这个技巧在电赛现场救过我很多次。
