
1. uFUN开发板初体验从开箱到上电第一次拿到uFUN开发板时这个仅有4×6cm大小的精致板卡确实让我有些惊讶。包装盒内除了开发板本体还附带了一根Micro USB数据线——这种设计非常贴心因为现在要找一根安卓接口的数据线反而越来越难了。板子正面最显眼的是那个RGB LED旁边是触摸按键区域。翻到背面可以看到STM32F103C8T6主控芯片这是STM32家族中非常经典的型号72MHz主频、64KB Flash和20KB RAM的配置对于学习嵌入式开发来说完全够用。板载的CH340G串口芯片意味着我们可以直接用USB线进行调试不需要额外的调试器。上电后预装的Demo程序让RGB LED开始循环变色触摸按键也有响应。我注意到板子上还有三轴加速度计LIS3DH、蜂鸣器和SD卡槽等外设这些都为后续开发提供了丰富的可能性。特别值得一提的是板子上的双T陷波滤波器设计这在音频信号处理实验中会非常有用。2. 开发环境搭建与基础测试2.1 工具链准备要为uFUN开发板编写程序首先需要搭建开发环境。我选择了Keil MDK作为IDE因为它对STM32系列的支持非常完善。安装时需要注意下载并安装Keil MDK建议5.26及以上版本安装STM32F1系列的Device Family Pack安装ST-Link驱动如果使用ST-Link调试器对于喜欢开源工具链的开发者也可以选择PlatformIO VSCode的方案。在PlatformIO中搜索STM32F103C8即可找到对应的开发板配置。2.2 第一个测试程序点亮LED让我们从最简单的LED控制开始。uFUN开发板上的RGB LED共阳极连接三个阴极分别通过限流电阻连接到MCU的PA8红、PA9绿和PA10蓝引脚。#include stm32f10x.h void Delay(uint32_t nCount) { for(; nCount ! 0; nCount--); } int main(void) { // 开启GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置PA8、PA9、PA10为推挽输出 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); while(1) { // 红灯亮 GPIO_SetBits(GPIOA, GPIO_Pin_9 | GPIO_Pin_10); GPIO_ResetBits(GPIOA, GPIO_Pin_8); Delay(5000000); // 绿灯亮 GPIO_SetBits(GPIOA, GPIO_Pin_8 | GPIO_Pin_10); GPIO_ResetBits(GPIOA, GPIO_Pin_9); Delay(5000000); // 蓝灯亮 GPIO_SetBits(GPIOA, GPIO_Pin_8 | GPIO_Pin_9); GPIO_ResetBits(GPIOA, GPIO_Pin_10); Delay(5000000); } }这个简单的程序让RGB LED依次显示红、绿、蓝三种颜色。注意这里的Delay函数是通过空循环实现的在实际项目中应该使用定时器来实现精确延时。3. 两只老虎音乐播放实现3.1 蜂鸣器驱动原理uFUN开发板上的蜂鸣器是无源蜂鸣器这意味着它需要外部提供振荡信号才能发声。与有源蜂鸣器不同无源蜂鸣器可以控制音高适合播放音乐。蜂鸣器连接在MCU的PB0引脚上我们可以通过PWM脉冲宽度调制来控制它。PWM的频率决定了音高而占空比则影响音量。3.2 音乐编程基础播放音乐需要解决两个问题音高控制通过不同频率的PWM实现节拍控制通过音符持续时间实现首先定义音符频率表#define NOTE_C4 262 #define NOTE_D4 294 #define NOTE_E4 330 #define NOTE_F4 349 #define NOTE_G4 392 #define NOTE_A4 440 #define NOTE_B4 494 #define NOTE_C5 523然后定义两只老虎的乐谱const uint16_t melody[] { NOTE_C4, NOTE_D4, NOTE_E4, NOTE_C4, NOTE_C4, NOTE_D4, NOTE_E4, NOTE_C4, NOTE_E4, NOTE_F4, NOTE_G4, NOTE_E4, NOTE_F4, NOTE_G4, NOTE_G4, NOTE_A4, NOTE_G4, NOTE_F4, NOTE_E4, NOTE_C4, NOTE_G4, NOTE_A4, NOTE_G4, NOTE_F4, NOTE_E4, NOTE_C4, NOTE_C4, NOTE_G3, NOTE_C4, NOTE_C4, NOTE_G3, NOTE_C4 }; const uint8_t noteDurations[] { 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 4, 4, 2, 8, 8, 8, 8, 4, 4, 8, 8, 8, 8, 4, 4, 4, 4, 2, 4, 4, 2 };3.3 PWM配置与音乐播放配置TIM3的通道3PB0为PWM输出void Buzzer_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 开启GPIOB和TIM3时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 配置PB0为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 定时器基础配置 TIM_TimeBaseStructure.TIM_Period 999; // ARR值 TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz/(711)1MHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // PWM模式配置 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; // 初始占空比50% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC3Init(TIM3, TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE); } void PlayNote(uint16_t frequency, uint32_t duration) { if(frequency 0) { TIM_SetCompare3(TIM3, 0); // 占空比为0无声 } else { uint16_t arr 1000000 / frequency - 1; uint16_t psc 71; TIM3-ARR arr; TIM3-PSC psc; TIM_SetCompare3(TIM3, arr / 2); // 50%占空比 } Delay(duration); TIM_SetCompare3(TIM3, 0); // 音符间隔静音 Delay(20000); }主程序中播放音乐int main(void) { Buzzer_Init(); while(1) { for(int i 0; i sizeof(melody)/sizeof(melody[0]); i) { uint32_t duration 1000000 / noteDurations[i]; PlayNote(melody[i], duration); } Delay(2000000); // 两小节间隔 } }4. 源代码深度解析与优化4.1 音乐播放器的设计思路上述实现虽然简单但有几个可以改进的地方使用定时器中断来精确控制音符时长而不是依赖不准确的Delay函数添加音量控制功能通过调整PWM占空比实现支持从SD卡读取乐谱文件实现可配置的音乐播放改进后的设计可以采用状态机模式typedef enum { MUSIC_STOP, MUSIC_PLAYING, MUSIC_PAUSE } MusicState; typedef struct { uint16_t freq; uint16_t duration; uint8_t volume; } Note; MusicState currentState MUSIC_STOP; uint16_t currentNote 0; Note song[32]; // 存储乐谱4.2 定时器中断实现精确计时配置TIM2用于节拍控制void TIM2_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_Prescaler 7199; // 10kHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); } void TIM2_IRQHandler(void) { if(TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); static uint16_t tick 0; tick; if(currentState MUSIC_PLAYING) { if(tick song[currentNote].duration) { tick 0; currentNote; if(currentNote sizeof(song)/sizeof(Note)) { currentNote 0; } PlayNote(song[currentNote].freq, song[currentNote].volume); } } } }4.3 添加音量控制功能修改PlayNote函数加入音量参数void PlayNote(uint16_t frequency, uint8_t volume) { if(frequency 0 || volume 0) { TIM_SetCompare3(TIM3, 0); } else { uint16_t arr 1000000 / frequency - 1; TIM3-ARR arr; TIM_SetCompare3(TIM3, arr * volume / 100); // 音量百分比 } }5. 进阶应用与扩展思路5.1 结合加速度计实现互动音乐uFUN开发板上的LIS3DH三轴加速度计可以用来实现有趣的互动效果。例如可以根据板子的倾斜角度改变音调或音量void CheckAccelerometer(void) { int16_t x, y, z; LIS3DH_ReadAcc(x, y, z); // 根据X轴倾斜调整音高 int16_t pitch NOTE_C4 (x / 100); pitch (pitch NOTE_C4) ? NOTE_C4 : pitch; pitch (pitch NOTE_C5) ? NOTE_C5 : pitch; // 根据Y轴倾斜调整音量 uint8_t vol 50 (y / 200); vol (vol 100) ? 100 : vol; vol (vol 10) ? 10 : vol; if(currentState MUSIC_PLAYING) { song[currentNote].freq pitch; song[currentNote].volume vol; PlayNote(pitch, vol); } }5.2 使用触摸按键控制播放uFUN开发板有一个触摸按键可以用来控制音乐播放void CheckTouchKey(void) { static uint8_t lastState 0; uint8_t currentState TouchKey_Read(); if(currentState !lastState) { // 触摸按键按下 if(currentState MUSIC_STOP) { currentState MUSIC_PLAYING; currentNote 0; } else if(currentState MUSIC_PLAYING) { currentState MUSIC_PAUSE; TIM_SetCompare3(TIM3, 0); // 静音 } else { currentState MUSIC_PLAYING; } } lastState currentState; }5.3 通过串口接收乐谱数据我们可以扩展功能通过串口接收乐谱数据并实时播放void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { static uint8_t recvBuf[32]; static uint8_t index 0; uint8_t data USART_ReceiveData(USART1); if(data \n) { // 解析乐谱数据 if(sscanf(recvBuf, %hu,%hu,%hhu, song[index].freq, song[index].duration, song[index].volume) 3) { index (index 1) % (sizeof(song)/sizeof(Note)); } memset(recvBuf, 0, sizeof(recvBuf)); index 0; } else if(index sizeof(recvBuf)-1) { recvBuf[index] data; } } }这样我们就可以通过串口终端发送类似262,500,50频率持续时间音量的数据来实时控制音乐播放了。6. 项目总结与经验分享通过这个uFUN开发板的音乐播放项目我总结了以下几点经验无源蜂鸣器的驱动与有源蜂鸣器不同无源蜂鸣器需要精确的频率控制才能发出不同音高。PWM的频率决定了音高而占空比则影响音量和音色。定时器的使用技巧在STM32中不同的定时器有不同特性。TIM1和TIM8是高级定时器TIM2-TIM5是通用定时器TIM6和TIM7是基本定时器。对于PWM输出通用定时器就足够用了。中断优先级的设置当使用多个中断如定时器中断和串口中断时需要合理设置优先级。音乐播放对时序要求较高因此定时器中断的优先级应该高于串口中断。电源噪声问题在调试过程中我发现蜂鸣器工作时会引入电源噪声影响其他外设。解决方法是在蜂鸣器电源引脚附近添加一个100μF的电解电容和一个0.1μF的陶瓷电容。STM32的时钟配置默认情况下STM32F103使用内部8MHz RC振荡器但为了获得更好的性能建议切换到外部晶振。uFUN开发板上有8MHz外部晶振可以通过修改系统时钟配置来使用。这个项目虽然简单但涵盖了STM32开发的多个重要知识点GPIO控制、定时器使用、PWM输出、中断处理、外设驱动等。通过扩展功能还可以学习到串口通信、加速度计使用等更高级的内容。uFUN开发板小巧但功能丰富非常适合嵌入式开发学习和原型验证。