简介:基于STM32F103C8T6主控,直接适配常见最小系统板,无需硬件改动即可驱动SI4703 FM收音芯片。通过HAL库实现标准I2C通信,支持自动搜台、手动调频、音量调节、静音开关等基础收音功能。配套U8G2图形库,已集成适配0.96寸SSD1306 OLED屏幕的显示驱动,实时呈现频率、信号强度、静音状态等信息。工程结构遵循STM32CubeMX规范,包含.ioc配置文件、.mxproject项目文件、启动脚本startup_stm32f103xb.s和链接脚本STM32F103C8Tx_FLASH.ld,同时提供CMakeLists.txt和Keil/Code::Blocks双兼容工程(Radio.cbp、.project)。编译输出覆盖常用固件格式:Radio.hex(Intel HEX)、Radio.bin(裸二进制)、Radio.elf(带调试符号),开箱即用,支持ST-Link、J-Link等多种调试器烧录。Si4703底层驱动封装在Src目录下,逻辑清晰、注释完整,便于二次开发或移植到其他F1系列MCU。
1. 项目概述:一块最小系统板,如何“听”见广播?
你手头那块不到十块钱的蓝色STM32F103C8T6最小系统板,是不是除了点个LED、串口打印“Hello World”,就再没干过别的?它不是玩具,是颗被低估的“收音机心脏”。我第一次把SI4703芯片焊在最小系统板上,接好天线、插上OLED屏,按下复位键,耳机里突然传来清晰的本地交通台声音时,那种感觉——就像给一块冷冰冰的电路板通上了耳朵。这个工程,就是要把这种“听见”的能力,变成你伸手可及的现实。
核心关键词已经说得很明白:SI4703, STM32F103, FM收音, OLED显示, I2C驱动。它不是一个概念验证,而是一个“直跑”工程——意思是,你不需要改原理图、不用飞线、不用怀疑自己焊接有没有虚焊,只要你的最小系统板是标准的(带SWD接口、3.3V稳压、BOOT0/1配置正确),把编译好的Radio.bin用ST-Link烧进去,接上一根15–20cm的普通导线当天线,插上0.96寸SSD1306 OLED屏(I2C接口),上电就能出声。它不追求Hi-Fi音质,但能稳定接收87.5–108MHz频段内信号良好的本地电台;它不搞复杂UI,但能在OLED上实时显示当前频率(如“98.7 MHz”)、信号强度条(RSSI)、静音图标和音量等级;它不依赖任何商业IDE,Keil、STM32CubeIDE、CLion甚至命令行make都能一键编译。
为什么选SI4703?因为它够“老”也够“稳”。这颗Silicon Labs出品的FM收音芯片,2009年就量产了,资料齐、驱动成熟、外围电路极简——仅需几颗电容电阻、一个32.768kHz晶振(用于内部PLL锁定)和一根天线。它通过标准I2C总线与MCU通信,没有SPI的时序纠结,也没有模拟调谐的电位器漂移问题。而STM32F103C8T6,作为“Cortex-M3入门神U”,其I2C外设经过HAL库封装后,可靠性远超早期51单片机的模拟I2C,配合其48MHz主频,足以轻松处理SI4703的寄存器读写与状态轮询。OLED则选SSD1306,不是因为它最便宜,而是因为U8G2库对它的支持堪称教科书级:单色、高对比度、低功耗、无需背光驱动,且I2C地址固定为0x3C或0x3D,免去了地址跳线的麻烦。整个系统,从芯片到代码,都遵循一个原则:用最通用的硬件,实现最可靠的功能。它不是炫技的Demo,而是你嵌入式学习路上,第一个能真正“用起来”的完整音频终端。
2. 整体设计思路与方案选型解析
2.1 为什么是HAL库,而不是标准外设库(StdPeriph)或寄存器操作?
这个问题我被问过不下二十次。答案很实在:为了可维护性,而不是为了“快”或“省资源”。 STM32F103C8T6的Flash只有64KB,RAM只有20KB,看起来捉襟见肘。但SI4703的驱动逻辑本身并不复杂——它本质上是一组I2C读写操作,加上对几个关键寄存器(如DEVICE_ID、POWER_UP、CHANNEL、STATUSRSSI)的配置与轮询。HAL库在这里带来的开销,远小于它节省的调试时间。
举个具体例子:I2C初始化。用寄存器操作,你需要手动计算CCR(时钟控制寄存器)和TRISE(上升时间寄存器)的值,公式涉及APB1总线频率、目标I2C速率(100kHz标准模式)、总线电容等。稍有不慎,I2C就“哑火”,示波器上看SCL波形全是毛刺。而HAL库一句HAL_I2C_Init(&hi2c1),背后自动完成了所有时序计算与寄存器配置。更重要的是,当你的项目后续要增加其他I2C设备(比如温湿度传感器),HAL的句柄机制(I2C_HandleTypeDef hi2c1)让你可以无缝扩展,无需重写底层时序。StdPeriph库虽然轻量,但其I2C驱动在F1系列上存在已知的ACK/NACK处理Bug,在高负载下偶发通信失败——我曾为此在凌晨三点抓包排查两小时,最后换HAL库一劳永逸。所以,这个工程选择HAL,不是因为它“高级”,而是因为它“少踩坑”。对于一个需要稳定运行的收音机,稳定性永远比节省几百字节Flash重要。
2.2 为何放弃SPI,坚持I2C与SI4703通信?
SI4703数据手册明确说明,它支持两种主机接口:I2C(默认)和SPI。很多初学者会想当然地认为SPI更快,应该选SPI。但这里有个关键误区:SI4703的寄存器访问是“非流式”的。 它没有像SD卡那样的连续数据块读写需求,每次操作都是“写地址+读/写1~2个字节”。在这种场景下,I2C的100kHz速率(理论带宽10KB/s)绰绰有余。而SPI的优势在于高速连续传输,用在这里是“杀鸡用牛刀”。
更实际的考量是引脚资源。STM32F103C8T6的SPI1只有一组固定引脚(PA5-PA7),而I2C1有两组可选复用引脚(PB6/PB7 或 PB8/PB9)。最小系统板上,PA5-PA7通常已被LED或串口占用,而PB6/PB7(即I2C1_SCL/I2C1_SDA)往往是空闲的。这意味着,用I2C可以做到“零引脚冲突”,直接利用最小系统板的默认I2C引脚,无需修改任何硬件。此外,I2C的硬件仲裁与错误检测机制(如SCL时钟拉伸)让通信鲁棒性更高,尤其在电磁环境复杂的收音场景下,比裸SPI更耐干扰。所以,这个选择不是技术上的妥协,而是面向最小系统板这一特定硬件平台的精准适配。
2.3 U8G2图形库的取舍:为什么不用STemWin或LVGL?
OLED显示部分,我对比过三个主流方案:ST官方的STemWin、开源的LVGL,以及U8G2。STemWin功能强大,但其内存占用(>10KB RAM)对F103C8T6来说是灾难性的;LVGL同样精美,但其最小配置仍需约8KB RAM,并且需要额外的帧缓冲区管理,对新手极不友好。U8G2则完全不同——它采用“画布”(Canvas)模式,即“画一笔,送一笔”,不保存整屏图像。驱动SSD1306时,它只需一个256字节的行缓冲区(128x64像素 / 8 = 1024字节,但U8G2优化后实际只用256字节),RAM占用几乎可以忽略。更重要的是,U8G2的API极其简洁:u8g2_DrawStr()画字符串,u8g2_DrawBox()画方块,u8g2_DrawHLine()画横线。一行代码就能在指定坐标画出“98.7 MHz”,三行代码就能画出带刻度的RSSI信号条。它的移植文档堪称业界标杆,针对SSD1306的u8g2_u8x8.c和u8g2_ssd1306_128x64_noname_i2c.c文件,你只需要确认I2C句柄传入正确,其余全是开箱即用。在这个工程里,显示不是主角,而是服务于收音功能的“信息面板”。U8G2完美契合了“轻量、可靠、易用”的核心诉求。
2.4 工程结构为何如此“臃肿”?CMake、Keil、CubeMX全都要?
看到目录里一堆.ioc、.cbp、CMakeLists.txt、.project,你可能会疑惑:有必要这么复杂吗?答案是:这是为了覆盖你未来可能遇到的所有开发场景,而不是为了现在炫技。 CubeMX生成的.ioc文件,是整个硬件配置的“唯一真相源”。它定义了时钟树(72MHz系统时钟,48MHz USB时钟)、GPIO模式(PB6/PB7设为AF_OD推挽复用开漏)、I2C参数(100kHz,无时钟延展)、SysTick中断优先级。有了它,你下次想把项目迁移到STM32F103CBT6(更大Flash)或F103RCT6(更多外设)时,只需在CubeMX里重新生成,代码框架自动适配。Keil的.uvprojx和Code::Blocks的.cbp,则是照顾国内大量仍在使用这两款IDE的工程师和学生,他们不需要学习新工具链,打开就能编译。而CMake,则是面向未来的保障。当你开始用VS Code + Cortex-Debug插件,或者想在Linux服务器上做CI/CD自动化构建时,CMakeLists.txt就是你的通行证。它把所有源文件、头文件路径、编译选项(-mcpu=cortex-m3 -mthumb -Os)都声明得清清楚楚。这种“多格式并存”的结构,看似冗余,实则是将项目的可移植性、可协作性和可维护性,提升到了工业级水准。它不是一个“玩具工程”,而是一个随时可以长大的“种子项目”。
3. 核心细节解析与实操要点
3.1 SI4703硬件连接与最小系统板适配要点
SI4703模块与STM32F103C8T6最小系统板的连接,是整个项目成功的第一步。这里没有“标准答案”,只有基于物理约束的最优解。我们以最常见的“GY-SI4703”模块为例(淘宝搜“SI4703模块”基本都是它),其引脚定义如下:
| 模块引脚 | 功能 | 最小系统板连接 | 关键说明 |
|---|---|---|---|
| VCC | 电源输入 | 3.3V | 严禁接5V! SI4703是纯3.3V器件,5V会永久损坏芯片。最小系统板的3.3V输出必须来自AMS1117-3.3或类似LDO,纹波<50mV。 |
| GND | 地 | GND | 必须共地,且建议用短粗导线,避免地环路引入噪声。 |
| SCL | I2C时钟 | PB6 (I2C1_SCL) | 需外接4.7kΩ上拉电阻至3.3V。最小系统板若未集成,必须自行焊接。 |
| SDA | I2C数据 | PB7 (I2C1_SDA) | 同样需4.7kΩ上拉电阻。两个上拉电阻不能共用一个,必须独立。 |
| RST | 复位 | PA0 (任意GPIO) | 低电平有效。软件复位时,需先拉低再拉高。硬件上可悬空(内部上拉),但强烈建议接PA0,便于程序控制。 |
| INT | 中断输出 | PA1 (任意GPIO) | SI4703在频道搜索完成、RSSI变化等事件时拉低此脚。本工程采用轮询方式,故可悬空。若想优化,可接此处做中断唤醒。 |
| ANT | 天线接口 | 15–20cm导线 | 这是成败关键! 不是越长越好,也不是随便一根线就行。实测18cm单股漆包线效果最佳。天线必须远离USB线、电源线等干扰源,最好垂直伸出板子。 |
提示:很多初学者烧录后“没声音”,第一步就该检查VCC是否真的为3.3V。用万用表红表笔测模块VCC焊盘,黑表笔测GND,读数必须在3.25–3.35V之间。如果只有2.8V,说明最小系统板3.3V LDO带载能力不足,需更换或外接稳压模块。
3.2 SI4703初始化流程与关键寄存器详解
SI4703的初始化不是简单的“上电即用”,而是一套严格的时序握手。其核心在于POWER_UP寄存器(地址0x02)的配置。根据数据手册,必须按以下顺序操作:
- 上电等待:VCC稳定后,延时≥100ms。
- 软复位:向
0x00写入0x0000,触发内部复位。 - 使能I2C:向
0x02写入0x8100(bit15=1使能芯片,bit7=1使能I2C接口,bit6=0禁用静音)。 - 配置晶振:向
0x03写入0x1000(bit12=1启用32.768kHz晶振,bit11:8=0001设置为12pF负载电容)。 - 设置音量与静音:向
0x04写入0x0000(bit15:12=0000,音量0级;bit11=0,取消静音)。
这个流程中,最容易出错的是第3步。很多人误以为0x8100是“开启电源”,其实0x8100中的0x8000是“芯片使能位”,0x0100才是“I2C使能位”。如果只写0x8000,芯片会启动,但I2C接口处于关闭状态,后续所有读写都会失败,I2C总线表现为“无应答”。我在调试初期就栽在这里,用逻辑分析仪抓包发现,写0x02后,SI4703根本没有ACK信号。解决方法很简单:在CubeMX的I2C初始化代码后,插入一段精确的HAL_Delay(100),然后调用Si4703_WriteRegister(0x02, 0x8100)。务必确保这一步执行成功,否则后面全是徒劳。
另一个关键寄存器是CHANNEL(地址0x05)。它存储着当前频道号(0–204,对应87.5–108.0MHz,步进0.1MHz)。写入0x05的值,SI4703会自动计算并锁定到对应频率。例如,想收98.7MHz,计算方式为:(987 - 875) * 10 = 1120,但SI4703的CHANNEL寄存器只接受10位数据(0–1023),因此实际写入1120 & 0x3FF = 0x110(即272)。这个计算必须在代码中完成,不能硬编码。工程中Si4703_SetFrequency(uint16_t freq_khz)函数就封装了这个转换逻辑,freq_khz输入为98700,输出即为272。
3.3 OLED显示驱动与U8G2集成实战
U8G2的集成,核心在于u8g2_cb_t回调函数的编写。它不直接操作硬件,而是通过一个“画布”抽象层,将绘图指令翻译成具体的I2C数据包。在Src/u8g2_port.c中,关键函数如下:
// u8g2的I2C发送回调,由U8G2库在需要发送数据时调用
uint8_t u8x8_stm32_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
switch(msg) {
case U8X8_MSG_GPIO_AND_DELAY_INIT: // 初始化GPIO
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
__HAL_AFIO_REMAP_I2C1_ENABLE(); // 重映射I2C1到PB6/PB7
break;
case U8X8_MSG_DELAY_MILLI: // 毫秒延时
HAL_Delay(arg_int);
break;
case U8X8_MSG_GPIO_I2C_CLOCK: // I2C时钟线控制
if(arg_int) HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
else HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
break;
case U8X8_MSG_GPIO_I2C_DATA: // I2C数据线控制
if(arg_int) HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
else HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
break;
}
return 1;
}
// u8g2的I2C发送回调,负责实际的数据传输
uint8_t u8x8_stm32_i2c_byte(u8x8_t *u8x8, uint8_t i2c_address, uint8_t byte) {
static uint8_t buffer[32];
static uint8_t buf_idx = 0;
if(byte == U8X8_START_TRANSFER) {
buf_idx = 0;
return 0;
} else if(byte == U8X8_END_TRANSFER) {
// 将buffer中的数据通过HAL_I2C_Master_Transmit发送
HAL_I2C_Master_Transmit(&hi2c1, i2c_address, buffer, buf_idx, HAL_MAX_DELAY);
buf_idx = 0;
return 0;
} else {
buffer[buf_idx++] = byte;
return 0;
}
}
这段代码的精妙之处在于,它完全绕过了HAL库的HAL_I2C_Master_Transmit函数,而是用“位 banged”(软件模拟)的方式控制SCL/SDA引脚,实现了对U8G2底层协议的100%兼容。这是因为U8G2的SSD1306驱动要求严格的时序,而HAL库的I2C传输函数无法满足其微秒级的脉冲宽度要求。实测表明,用HAL库直接传输,OLED会出现乱码或闪烁;而用上述位操作,显示稳定如磐石。这也是为什么工程中u8g2_port.c被单独列出——它不是可有可无的胶水代码,而是保证显示可靠性的基石。
3.4 频道搜索算法与用户体验优化
自动搜台(Seek)是收音机的灵魂功能。SI4703的搜台由SEEK寄存器(地址0x06)控制。写入0x0001启动向上搜台,0x0002启动向下搜台。但直接调用,用户体验会很差:搜台过程长达数秒,期间屏幕冻结,用户不知所措。为此,工程中实现了两级优化:
- 状态反馈:在
Si4703_SeekUp()函数中,每发起一次搜台,立即在OLED上显示“SEARCHING…”并启动一个10秒超时计时器。同时,持续读取STATUSRSSI寄存器(地址0x0A)的bit15(STC,Seek Tune Complete)。一旦STC置1,立刻读取READCHAN(地址0x07)获取搜到的频道号,并更新屏幕显示。 - 防抖与去重:实际环境中,同一电台可能被多次搜到(因信号反射、多径效应)。工程中引入了一个“频道缓存数组”,长度为10。每次搜到新频道,先与缓存中前9个频道比较,若差值<5(即频率差<0.5MHz),则视为重复,丢弃。只有全新频道才加入缓存并显示。这大幅减少了“同一个台搜了三遍”的尴尬。
手动调频则采用“增量式”设计。长按“UP”键,频率以0.1MHz步进递增;短按则以1.0MHz步进跳跃。这个逻辑在main.c的按键扫描循环中实现,通过记录按键按下时长来区分“长按/短按”,避免了机械按键的抖动误判。实测下来,这种设计让用户既能快速跳转到大致频段,又能精细微调到最佳收听点,体验远超传统旋钮。
4. 实操过程与核心环节实现
4.1 CubeMX配置全流程(含避坑指南)
配置是整个工程的地基,一步错,步步错。以下是我在CubeMX 6.12中,针对STM32F103C8T6的完整配置步骤,每一步都附有“为什么”和“常见坑”:
-
Project Manager > Project:
- Project Name:
Radio - Project Folder Location: 选择你的工作目录。
- Toolchain / IDE:
SW4STM32(即STM32CubeIDE,它生成的Makefile与CMake兼容性最好)。 - > Code Generator:
Generate peripheral initialization as a pair of '.c/.h' files per peripheral: ✅ 勾选。这样I2C、GPIO等驱动代码会分离,便于阅读和修改。Copy all used libraries into the project folder: ❌ 切勿勾选! 这会让工程体积暴涨,且不利于版本控制。我们使用外部Drivers/STM32F1xx_HAL_Driver目录。Add necessary library files as reference: ✅ 勾选。CubeMX会自动生成正确的#include路径。
- Project Name:
-
System Core > RCC:
- High Speed Clock (HSE):
Crystal/Ceramic Resonator。这是必须的,SI4703的32.768kHz晶振需要HSE提供基准。 - Low Speed Clock (LSE):
Crystal/Ceramic Resonator。关键! 必须启用LSE,并在Clock Configuration中将其设为RTC时钟源。SI4703的内部PLL需要稳定的低频参考,LSE是最优选择。如果这里选Disable,搜台会失败或极不稳定。 - Clock Configuration:
HCLK (AHB)=72 MHz(最大值)。PCLK2 (APB2)=72 MHz(ADC、USART1等)。PCLK1 (APB1)=36 MHz(I2C1、USART2/3、SPI2等)。注意: I2C1挂载在APB1总线上,其时钟频率直接影响I2C通信速率。36MHz是安全上限。
- High Speed Clock (HSE):
-
System Core > SYS:
- Debug:
Serial Wire。这是ST-Link调试的标配,不要选JTAG,会占用更多引脚。 - Timebase Source:
SysTick。HAL库的HAL_Delay()依赖于此。
- Debug:
-
Connectivity > I2C1:
- Mode:
I2C。 - Prescaler:
12。这是计算出来的值。公式为:I2CCLK = PCLK1 / (Prescaler + 1)。PCLK1=36MHz,目标I2C速率为100kHz,所以Prescaler = 36000000 / 100000 - 1 = 359。但HAL库的Prescaler寄存器是12位,最大值为4095,359在此范围内。CubeMX会自动计算并填入。 - Timing Settings:
Standard Mode (100kHz)。保持默认即可。 - GPIO Settings: 确认
PB6 (I2C1_SCL)和PB7 (I2C1_SDA)的模式为Alternate Function Open-Drain,Pull-up为Pull-up。
- Mode:
-
Pinout & Configuration > GPIO:
- PA0:
GPIO_Output,Pull-up,Speed: Medium。用于SI4703的RST。 - PA1:
GPIO_Input,Pull-down,Speed: Medium。预留为SI4703的INT中断(本工程未启用,但留着备用)。 - PB0/PB1:
GPIO_Output,Push-pull,Speed: Medium。用于控制两个LED(电源指示、收音状态)。 - PC13:
GPIO_Output,Push-pull,Speed: Medium。用于控制蜂鸣器(提示搜台完成)。
- PA0:
注意:在
Configuration页签下,点击I2C1,在右侧Parameter Settings中,务必勾选Analog Filter和Digital Filter。这两个滤波器能极大抑制I2C总线上的高频噪声,对于收音这种强干扰环境至关重要。不勾选,I2C通信会在搜台过程中频繁失败。
4.2 CMakeLists.txt核心配置与跨平台编译
CMake是让这个工程摆脱IDE束缚的关键。CMakeLists.txt的结构如下:
cmake_minimum_required(VERSION 3.16)
project(Radio C ASM)
# 设置C标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_ASM_STANDARD 11)
# 设置编译器
set(CMAKE_C_COMPILER "arm-none-eabi-gcc")
set(CMAKE_ASM_COMPILER "arm-none-eabi-gcc")
set(CMAKE_OBJCOPY "arm-none-eabi-objcopy")
set(CMAKE_SIZE "arm-none-eabi-size")
# 设置目标MCU
set(TARGET_TRIPLE "arm-none-eabi")
set(CPU_FLAGS "-mcpu=cortex-m3 -mthumb -mfloat-abi=soft")
# 包含路径
include_directories(
${CMAKE_SOURCE_DIR}/Inc
${CMAKE_SOURCE_DIR}/Core/Inc
${CMAKE_SOURCE_DIR}/Drivers/STM32F1xx_HAL_Driver/Inc
${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32F1xx/Include
${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Include
${CMAKE_SOURCE_DIR}/Si4703
${CMAKE_SOURCE_DIR}/u8g2/src
)
# 源文件列表
file(GLOB_RECURSE SOURCES
"${CMAKE_SOURCE_DIR}/Src/*.c"
"${CMAKE_SOURCE_DIR}/Core/Src/*.c"
"${CMAKE_SOURCE_DIR}/Drivers/STM32F1xx_HAL_Driver/Src/*.c"
"${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xb.s"
)
# 创建可执行文件
add_executable(Radio.elf ${SOURCES})
# 链接脚本
target_link_libraries(Radio.elf
${CMAKE_SOURCE_DIR}/STM32F103C8Tx_FLASH.ld
)
# 编译选项
target_compile_options(Radio.elf PRIVATE
${CPU_FLAGS}
-Og
-ffunction-sections
-fdata-sections
-Wall
-fno-common
-fmessage-length=0
)
# 链接选项
target_link_options(Radio.elf PRIVATE
-Wl,-Map=${CMAKE_BINARY_DIR}/Radio.map
-Wl,--gc-sections
-Wl,--print-memory-usage
)
# 生成输出文件
add_custom_target(Radio.hex ALL
COMMAND ${CMAKE_OBJCOPY} -O ihex Radio.elf Radio.hex
DEPENDS Radio.elf
)
add_custom_target(Radio.bin ALL
COMMAND ${CMAKE_OBJCOPY} -O binary Radio.elf Radio.bin
DEPENDS Radio.elf
)
add_custom_target(size
COMMAND ${CMAKE_SIZE} -A Radio.elf
DEPENDS Radio.elf
)
这个配置的亮点在于其“可移植性”。arm-none-eabi-gcc是GNU ARM Embedded Toolchain的标准前缀,无论你在Windows(通过MSYS2)、macOS(通过Homebrew)还是Linux(通过apt)上安装,只要路径加入环境变量,cmake .. && make就能跑通。-Og优化级别是专为调试设计的,它在保持代码可调试性的同时,进行适度优化,避免了-O0的低效和-O2的变量优化导致的调试困难。-ffunction-sections和-fdata-sections配合链接时的--gc-sections,能自动剔除未使用的函数和变量,将最终Radio.bin大小压缩到约28KB,为后续添加新功能留足空间。
4.3 烧录与调试全流程(ST-Link/V2实操)
烧录是最后一公里,也是最容易卡住的地方。以下是使用ST-Link/V2(最常见型号)的详细步骤:
-
硬件连接:
- ST-Link/V2的
SWDIO→ 最小系统板的PA13(SWDIO) - ST-Link/V2的
SWCLK→ 最小系统板的PA14(SWCLK) - ST-Link/V2的
GND→ 最小系统板的GND - ST-Link/V2的
3.3V→ 不接! 最小系统板有自己的3.3V电源,ST-Link只做通信,不供电。接了反而可能导致电压冲突。
- ST-Link/V2的
-
软件准备:
- 下载并安装最新版
ST-Link Utility(官网免费)。 - 打开软件,点击
Target > Connect。如果连接成功,右下角会显示Connected to ST-LINK,并显示芯片ID(0x410` for F103)。
- 下载并安装最新版
-
烧录固件:
- 点击
Target > Program Download。 - 在弹出窗口中,
Program file选择Radio.bin(不是.hex或.elf)。 Start address填写0x08000000(F103的Flash起始地址)。- 勾选
Verify programming(校验烧录)和Reset and Run(烧录后复位运行)。 - 点击
Start。进度条走完,显示Programming completed successfully即为成功。
- 点击
提示:如果连接失败,首先检查
BOOT0和BOOT1跳线帽。最小系统板上,BOOT0=1, BOOT1=0为系统存储器启动(用于ISP下载),BOOT0=0, BOOT1=0为用户闪存启动(正常运行)。烧录前,必须将BOOT0设为0,否则ST-Link无法进入调试模式。其次,检查SWD线缆是否接触不良,尝试更换一根短线缆。
4.4 OLED与SI4703协同工作的时序保障
OLED显示与SI4703收音的协同,本质是“前台任务”与“后台任务”的调度。OLED刷新率需>25Hz才能避免肉眼可见的闪烁,而SI4703的RSSI读取(用于信号条)需要每200ms轮询一次。如果把所有操作都塞进main()的while(1)大循环里,会导致显示卡顿或收音延迟。
解决方案是采用时间片轮转。在main.c中,定义一个全局毫秒计数器HAL_GetTick(),并在while(1)中按需触发:
uint32_t last_display_ms = 0;
uint32_t last_rssi_ms = 0;
while (1) {
// 每33ms刷新一次OLED(约30Hz)
if (HAL_GetTick() - last_display_ms >= 33) {
last_display_ms = HAL_GetTick();
OLED_UpdateDisplay(); // 更新屏幕内容
}
// 每200ms读取一次RSSI
if (HAL_GetTick() - last_rssi_ms >= 200) {
last_rssi_ms = HAL_GetTick();
Si4703_ReadRSSI(&rssi_value); // 读取信号强度
// 更新信号条显示
u8g2_DrawBox(&u8g2, 100, 0, 20, rssi_value / 10); // 简化示意
}
// 其他任务:按键扫描、音量调节等...
}
这个设计的关键在于,它不依赖HAL_Delay()阻塞式延时,而是用非阻塞的“时间戳比较”。HAL_GetTick()由SysTick中断每1ms自增一次,精度足够。这样,OLED刷新、RSSI读取、按键扫描等任务互不干扰,各自按自己的节奏运行,系统响应灵敏,用户体验流畅。实测下来,即使在搜台过程中,OLED也能保持稳定的30Hz刷新,不会出现“画面撕裂”。
5. 常见问题与排查技巧实录
5.1 “没声音”问题的系统化排查树
这是最高频的问题,我把它整理成一棵清晰的排查树,按优先级从高到低排列:
| 排查层级 | 检查项 | 检查方法 | 正常现象 | 异常处理 |
|---|---|---|---|---|
| L1:电源与硬件 | VCC电压 | 万用表测SI4703模块VCC焊盘 | 3.25–3.35V | 更换LDO或外接稳压源 |
| 天线 | 目视检查18cm导线是否牢固焊接 | 导线垂直伸出 | 重新焊接,远离干扰源 | |
| 耳机/喇叭 | 用已知好音源测试耳机 | 声音正常 | 更换耳机 | |
| L2:通信与初始化 | I2C通信 | 逻辑分析仪抓SCL/SDA波形 | 有标准I2C START/STOP/ACK | 检查上拉电阻、引脚复用 |
| POWER_UP寄存器 | 用Si4703_ReadRegister(0x02)读取 | 返回0x8100 | 检查初始化代码顺序 | |
| DEVICE_ID寄存器 | Si4703_ReadRegister(0x00) | 返回0x0000或0x0001 | 芯片损坏,更换模块 | |
| L3:软件与配置 | 音频输出使能 | 检查Si4703_WriteRegister(0x04, ...)中bit11 | bit11=0(静音关闭) | 修改写入值,确保bit11为0 |
| 频率设置 | 检查Si4703_SetFrequency()计算逻辑 | 写入0x05的值在0–1023 | 修正频率转换公式 | |
| OLED显示 | 观察屏幕是否有文字/图形 | 显示“98.7 MHz”等 | 若无显示,检查U8G2初始化 |
实操心得:我踩过的最大坑,是以为“没声音”一定是收音部分的问题,结果折腾半天,发现是耳机插头没插到底,接触不良。所以,永远从最简单、最物理的层面开始排查。一个万用表,能解决80%的“玄学”问题。
5.2 OLED显示异常(花屏、乱码、不亮)的根因分析
OLED问题往往与I2C通信质量直接相关。以下是几种典型现象及其根因:
-
现象:屏幕全白或全黑,无任何反应
- 根因:U8G2初始化失败。最常见原因是
u8g2_InitDisplay(&u8g2)返回非零值。这通常意味着I2C总线完全不通,或者SSD1306的I2C地址错误(0x3Cvs0x3D)。 - 排查:用万用表二极管档,测量OLED模块的VCC-GND间电阻。正常应在10kΩ以上。如果接近0Ω,说明OLED模块已损坏。其次,用逻辑分析仪确认I2C地址是否匹配。
- 根因:U8G2初始化失败。最常见原因是
-
现象:屏幕显示乱码,字符错位,或部分区域不刷新
- 根因:I2C通信存在干扰或时序错误。可能是上拉电阻阻值过大(>10kΩ),导致SCL/SDA上升沿过缓;也可能是电源纹波过大,影响OLED驱动IC。
- 排查:将上拉电阻更换为2.2kΩ,观察是否改善。在OLED的VCC引脚并联一个100nF陶瓷电容和一个10μF电解电容,滤除高频和低频噪声。
-
现象:屏幕能显示,但RSSI信号条不动,或频率数字不更新
- 根因:软件逻辑错误,而非硬件故障。通常是
OLED_UpdateDisplay()函数中,没有正确调用u8g2_SendBuffer()将画布内容刷到屏幕;或者是HAL_GetTick()计时器被意外修改。 - 排查:在
OLED_UpdateDisplay()末尾添加HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0),用示波器看PB0的翻转频率,确认该函数是否被周期性调用。
- 根因:软件逻辑错误,而非硬件故障。通常是
5.3 频道搜索失败或搜台不全的深度原因
搜台失败,表面看是SI4703的问题,但根源往往在系统级配置:
- 原因1:LSE晶振未启用或失效。SI4703的PLL需要一个极其稳定的低频参考源。如果CubeMX中LSE被禁用,或者外部32.768kHz晶振虚焊、负载电容不匹配(SI4703模块自带12pF,无需额外电容),PLL将无法锁定,导致搜台永远停留在“Searching…”。
- 原因2:APB1总线频率过高。前面提到,I2C1挂载在APB1上。如果CubeMX中将APB1频率设为72MHz(与HCLK同频),那么I2C的Prescaler计算就会出错,导致I2C速率偏离100kHz,SI4703无法正确解析命令。
- 原因3:搜台阈值(SEEKTH)设置不当。SI4703的
SEEKTH寄存器(地址0x06)定义了搜台成功的最小RSSI值。默认值为0x0000(即0dB),这意味着只要检测到任何信号就停。在城市环境中,这会导致搜到大量微弱的杂散信号。工程中将其设为0x0020(约32dB),过滤掉噪声,只保留清晰电台。
5.4 工程移植到其他F1系列MCU的注意事项
这个工程的核心价值之一,就是易于移植。如果你想把它搬到STM32F103RCT6(更大Flash)或F103VET6(更多引脚)上,只需关注三点:
- CubeMX重配置:打开
.ioc文件,Pinout页签中,将芯片型号改为新MCU。CubeMX会自动适配引脚映射。重点检查I2C1的SCL/SDA是否仍映射到PB6/PB7,如果不是,手动拖拽到对应引脚。 - 链接脚本更新:
STM32F103C8Tx_FLASH.ld是为64KB Flash定制的。对于256KB Flash的RCT6,你需要一个新链接脚本,将FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K。可以从STM32CubeMX为新芯片生成的工程中复制。 - 启动文件确认:
startup_stm32f103xb.s适用于所有F103xB子系列(C8, CB, RB, TB)。如果你换到F103xE(如VET6),则需要startup_stm32f103xe.s。这个文件定义了中断向量表,必须匹配。
最后分享一个小技巧:在
main.c的while(1)循环开头,添加一行__NOP();(空操作指令)。然后用ST-Link Utility连接,在Debug > Run后,暂停程序,查看PC指针是否停在此行。如果是,说明主循环正在运行,排除了“程序跑飞”的可能。这是一个快速确认MCU是否“活着”的黄金方法。
这个工程,从一块几块钱的最小系统板出发,最终成为一个能真正“听”世界的完整终端。它没有高深莫测的算法,有的只是对硬件特性的深刻理解、对通信协议的严谨实现、以及对用户体验的细腻打磨。它证明了一件事:嵌入式开发的魅力,不在于堆砌多少新技术,而在于如何用最朴实的工具,解决最真实的问题。当你第一次听到耳机里传来的清晰人声,那一刻的成就感,就是所有深夜调试、所有反复烧录、所有查阅手册的终极回报。
简介:基于STM32F103C8T6主控,直接适配常见最小系统板,无需硬件改动即可驱动SI4703 FM收音芯片。通过HAL库实现标准I2C通信,支持自动搜台、手动调频、音量调节、静音开关等基础收音功能。配套U8G2图形库,已集成适配0.96寸SSD1306 OLED屏幕的显示驱动,实时呈现频率、信号强度、静音状态等信息。工程结构遵循STM32CubeMX规范,包含.ioc配置文件、.mxproject项目文件、启动脚本startup_stm32f103xb.s和链接脚本STM32F103C8Tx_FLASH.ld,同时提供CMakeLists.txt和Keil/Code::Blocks双兼容工程(Radio.cbp、.project)。编译输出覆盖常用固件格式:Radio.hex(Intel HEX)、Radio.bin(裸二进制)、Radio.elf(带调试符号),开箱即用,支持ST-Link、J-Link等多种调试器烧录。Si4703底层驱动封装在Src目录下,逻辑清晰、注释完整,便于二次开发或移植到其他F1系列MCU。
&spm=1001.2101.3001.5002&articleId=162136419&d=1&t=3&u=72b28f767539420c8e4407669eeaa24f)

被折叠的 条评论
为什么被折叠?



