简介:一套完整的基于STM32微控制器与Si4703 FM收音机芯片的嵌入式项目资源,面向有一定单片机基础、希望学习FM收音机驱动或I2C/SPI通信开发的电子爱好者与工程师,也可用于嵌入式课程设计或毕业设计参考。资源包共15个文件,约97KB,包含PCB制造所需的Gerber与钻孔文件、STM32驱动源码(.h/.cpp/.ino)、3D打印外壳STL模型以及PDF格式的原理图说明,覆盖硬件、软件与结构设计,便于直接参考或二次开发。内容围绕Si4703的寄存器配置、广播接收与RDS数据处理展开,展示了如何通过STM32控制芯片实现频率搜索、播放与信息解析;同时涉及GPIO、I2C/SPI等外设接口的初始化与通信流程,以及驱动代码的编写思路。配套的板级Gerber文件和STL外壳模型让读者可以实际打样与装配,快速搭建可运行的原型机。目前已有457人学习下载,适合作为入门到进阶的实战参考资料。
1. 老芯片 Si4703 和 STM32 的组合,图什么
如果你手头有一颗 Si4703 FM 收音机芯片,又有一块 STM32,别急着换 RDA5807。Si4703 虽然老,但它把接收链路里的 RSSI、AFC、RDS 和去加重的状态全部拆在 I2C 寄存器里,比那种一上来就全部自动、不给调试入口的数字收音芯片更适合做实验。我最初就是从一颗 Si4703 模块把 FM 收音机点亮的,这个标题下最常被问到的不是“能不能响”,而是“为什么按例程写了还是不工作”。
这篇文章会从硬件接线、I2C 复位时序、初始化寄存器、手动调台和自动搜台一路讲到排查,重点放在你照着做会遇到的实际问题上:芯片没被复位、I2C 地址不对、STC 位永远不置位、搜台搜过头。适合刚用 STM32 调 I2C 外设的工程师,也适合打算把收音机塞进自己板子的人。
先说一个反直觉的结论:Si4703 上电后不能直接写寄存器,必须先拉低再拉高 RST,等内部 LDO 稳定后再写电源控制寄存器,否则后续所有写入都可能丢字节。这个顺序错了,后面读回全是 0xFF 也别奇怪。下面是完整落地过程。
2. 硬件接线和电源顺序:决定 Si4703 能不能被 I2C 叫醒
2.1 Si4703 最小系统:供电、I2C 和复位引脚
很多 Si4703 模块会把周边电路做齐,但如果你是在自制板子上用,最小系统需要保证的引脚有下面这些,缺一个都不行。
| 引脚 | 方向 | 接线说明 |
|---|---|---|
| VDD | 电源输入 | 建议 3.3V,串一个 10Ω 磁珠再接芯片,再并 10uF + 0.1uF 去耦 |
| GND | 地 | 所有地共同回到电源负极,特别是耳机座地 |
| SDA | I2C 数据 | 接 STM32 I2C 引脚,外部上拉 4.7kΩ 到 3.3V |
| SCL | I2C 时钟 | 接 STM32 I2C 引脚,外部上拉 4.7kΩ 到 3.3V |
| RST | 复位/使能 | 接 STM32 普通 GPIO,输出模式,不能悬空 |
| RCLK | 参考时钟输入 | 接 32.768kHz 晶振或外部有源时钟 |
| LOUT / ROUT | 音频输出 | 通过 22uF 电容接到耳机座左右声道 |
| GPIO2 / GPIO3 | 模块功能脚 | 可以接 LED 或按键,不接也行 |
这里的 RST 不是普通复位脚,它参与 I2C 总线的切换。Si4703 内部没有一个完整的上电复位电路,外部电压上来以后必须由 MCU 拉低 RST 再释放,芯片才会进入 I2C 控制模式。如果 RST 悬空,或者只是拉到 VDD 上,芯片可能停留在内部测试模式,I2C 不产生 ACK。
RCLK 或 32.768kHz 晶振必须有。这个时钟是芯片内部数字状态机和调谐频点计算的基准。常见 Si4703 模块会用一颗 32.768kHz 晶振,很多排针模块也把 RCLK 引出,让你自己接有源时钟。没有 RCLK 信号时,STC 位永远不会置位,调台会卡死,这是新手最容易忽略的问题。
多数 Si4703 的 VDD 可以工作在 2.7V 到 5.5V,但我建议统一用 3.3V,省掉电平转换。STM32 如果是 3.3V 系统,I2C 的 SDA/SCL 直接接上去就行。如果你用的是 5V 供电的 STM32 开发板,两个 I2C 引脚需要串联 1kΩ 电阻限流,否则 Si4703 的 SDA/SCL 引脚会被拉到超过绝对最大额定值。
2.2 三个容易翻车的位置:天线、去耦、耳机地
第一是天线。Si4703 在射频前端没有额外的 LNA,天线焊盘需要一个真实的天线。最简单的做法是把耳机座的地线当成天线地,耳机线本身当作 1/4 波长接收天线。LOUT 和 ROUT 各接一个 22uF 电解电容,电容另一端接耳机插头顶端和环,然后耳机地回到板子 GND。别把 LOUT 直接短路到地,那样静音和音量控制会变得很难听。
第二是去耦。Si4703 的射频部分对电源噪声敏感,我见过一块板子只在 VDD 放了一个 0.1uF,结果收低段台的时候声音轻,高段台又正常。加上 10uF 钽电容和 0.1uF 陶瓷电容之后问题消失。建议电源走线先经过磁珠再进 VDD,并确保去耦电容离 Si4703 VDD 引脚越近越好。
第三是耳机地。如果你的设备使用电池供电,耳机插座的 GND 一定要和电源 GND 接到同一个参考地。若耳机地悬空,天线回路不稳定,声音会断续,甚至 RST 复位瞬间会把地弹打到 MCU 的复位脚上。我习惯在耳机插座地到电源地之间串一个 1kΩ 电阻再加一个 100nF 电容并联,这种接法能减少插拔耳机时的电流声,但代价是天线地没那么实,所以你可以根据自己板子决定取舍。
3. STM32 上的 Si4703 I2C 驱动:复位、写寄存器、读回校验
3.1 一次 I2C 事务里发什么:0x02 开头,32 字节收尾
Si4703 的 I2C 协议和普通 EEPROM 不一样。很多人第一次调它时,像读写 24C02 那样写一个寄存器地址然后写一个数据,结果芯片没有反应,因为 Si4703 需要你把所有寄存器作为一个整体块写入。
写入时,第一个字节传一个命令索引 0x02,后面紧跟着 16 个寄存器的值,每个值高字节在前。这 16 个寄存器从芯片内部地址 0x02 开始,包含了 POWERCFG、CHANNEL、SYSCONFIG1、SYSCONFIG2、SYSCONFIG3 等等。你可以只写其中几个,但一次事务必须把 16 个寄存器都带上,否则芯片内部指针会乱。
读取时,同样先发一个字节 0x02,然后主机侧发起读操作,连续读取 32 字节。读回来 16 个寄存器值,寄存器 0x0A 是 STATUS,里面有 STC 和 RDS 标志位,寄存器 0x0E 的高 4 位是 RSSI。我下面给出的读写封装就是围绕这个协议设计的。
3.2 可复用的读写封装和初始化序列
下面这段代码基于 STM32 HAL 库。如果你是用标准库新建工程,原理一样,只需要把HAL_I2C_Master_Transmit换成你自己的 I2C 发送函数。
// si4703_device.c —— 基于 STM32 HAL 的 Si4703 驱动骨架 #include "main.h" #define SI4703_I2C_ADDR (0x10 << 1) // 7-bit 地址 0x10,左移一位给 HAL 用 #define SI4703_RST_PORT SI4703_RST_GPIO_Port #define SI4703_RST_PIN SI4703_RST_Pin static I2C_HandleTypeDef *si4703_i2c; static uint16_t si4703_regs[16]; // 寄存器镜像 static uint8_t si4703_rx[32]; int si4703_write_all(void) { uint8_t tx[33]; tx[0] = 0x02; // 写入命令索引,必须从 0x02 开始 for (int i = 0; i < 16; i++) { tx[1 + i * 2] = (si4703_regs[i] >> 8) & 0xFF; tx[2 + i * 2] = si4703_regs[i] & 0xFF; } if (HAL_I2C_Master_Transmit(si4703_i2c, SI4703_I2C_ADDR, tx, 33, 100) != HAL_OK) return -1; return 0; } int si4703_read_all(void) { uint8_t cmd = 0x02; if (HAL_I2C_Master_Transmit(si4703_i2c, SI4703_I2C_ADDR, &cmd, 1, 100) != HAL_OK) return -1; if (HAL_I2C_Master_Receive(si4703_i2c, SI4703_I2C_ADDR, si4703_rx, 32, 100) != HAL_OK) return -1; for (int i = 0; i < 16; i++) si4703_regs[i] = (si4703_rx[i * 2] << 8) | si4703_rx[i * 2 + 1]; return 0; } void si4703_hw_reset(void) { HAL_GPIO_WritePin(SI4703_RST_PORT, SI4703_RST_PIN, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(SI4703_RST_PORT, SI4703_RST_PIN, GPIO_PIN_SET); HAL_Delay(500); } void si4703_init(I2C_HandleTypeDef *hi2c) { si4703_i2c = hi2c; // 第一步:硬件复位,等内部 LDO 起来 si4703_hw_reset(); // 第二步:只写 POWERCFG 的 ENABLE 位,让芯片先进入工作态 memset(si4703_regs, 0, sizeof(si4703_regs)); si4703_regs[0x02] = 0x0001; // bit0 = ENABLE,参考数据手册 si4703_write_all(); HAL_Delay(50); // 第三步:写完整配置 // 这里的位值对应你板子上的频段和去加重, // 不同版本芯片可能存在位定义差异,务必对照数据手册 si4703_regs[0x02] = 0x0001; // ENABLE si4703_regs[0x03] = 0x0000; // CHANNEL,先不调台 si4703_regs[0x04] = 0xC000; // SYSCONFIG1:去加重 75us si4703_regs[0x05] = 0x4020; // SYSCONFIG2:BAND=US, SPACE=200kHz, SEEKTH=0x20 si4703_regs[0x06] = 0x0000; // SYSCONFIG3:先关 RDS si4703_write_all(); HAL_Delay(100); si4703_read_all(); // 读回,确认 I2C 链路正常 }代码里的数组下标直接采用寄存器地址 0x02、0x03、0x04、0x05、0x06。你会发现这和很多现成驱动的数组下标不太一样,那是因为那些驱动把读回来的 16 个字从 0 开始排,而写操作却依然从 0x02 这个地址开始。移植别人代码时,第一件事就是确认数组下标和寄存器地址的对应关系。
初始化时先写 POWERCFG 的 ENABLE,等 50ms,再写完整配置。这个分步逻辑很重要:Si4703 在一上电时内部很多模拟模块还没稳定,直接写频率和系统配置可能丢字节。我见过有人把复位后延迟从 500ms 砍到 10ms,结果 100 片板子里有 20 片 I2C 读回全 0xFF,后来把延迟加回去就正常了。
3.3 初始化参数的几个关键位
SYSCONFIG1(0x04)的 bit15 和 bit14 是去加重时间选择。日本以外 FM 广播一般用 75us,我国和欧洲部分地区使用 50us。选错了人声会发尖,声音不耐听。如果你拿到的模块默认是 75us 但当地电台要求 50us,改这里就行。
SYSCONFIG2(0x05)的 bit7 到 bit4 是 SEEKTH 阈值,bit3 到 bit0 是 BAND 和 SPACE。US 频段是 87.5MHz 到 108MHz,SPACE=00 表示 200kHz 步进。如果你要把模块卖到日本市场,BAND 要改成 01,否则自动搜台时会一直绕到频段边界再回头。SEEKTH 的默认值 0x20 偏小,实际搜台时容易把噪声当成电台,我一般从 0x30 开始调。
SYSCONFIG3(0x06)里面有软静音和立体声相关位。如果你在调试阶段听不到声音,先把软静音关掉,也就是把这个寄存器的相应位置 0,否则在信号临界时会听到明显的音量忽大忽小。
4. 手动调台和自动搜台:频率换算、SEEK 状态机与 RSSI 阈值
4.1 频率到 CHAN 的换算公式以及步进选择
Si4703 的 CHANNEL 寄存器里存的不是频率本身,而是一个频道序号。在 SPACE=200kHz、BAND=US 的设置下,简单的换算关系是:
CHAN = (实际频率 MHz - 87.5) / 0.2
比如接收 100.7MHz,CHAN = (100.7 - 87.5) / 0.2 = 66,转成二进制写进 CHANNEL 寄存器第 13 到第 0 位。如果 SPACE=100kHz,那么步长是 0.1MHz,换算公式变成 CHAN = (实际频率 MHz - 87.5) / 0.1。
在代码里建议用整数运算,避免浮点误差。把频率用千赫做单位,比如 100.7MHz 就是 100700kHz,然后先减 87500,再除以步长。这样得到的 CHAN 是整数,不会出现浮点精度导致的边界漂移。
一个常见的玄学问题:CHAN 算出来是 66,写进寄存器后显示 100.7MHz,但实际收到的台可能是 100.5 或 100.9。这通常是 Si4703 内部参考频率偏移导致,先把 RCLK 换成年份新的 20ppm 晶振,再考虑去动 AFC 位。
4.2 代码实现:手动调谐到指定频率并等待 STC
void si4703_tune_freq_khz(uint16_t freq_khz) { uint16_t chan; uint16_t step_khz = 200; // 必须和 SYSCONFIG2 里的 SPACE 一致 if (freq_khz < 87500 || freq_khz > 108000) return; chan = (uint16_t)((freq_khz - 87500) / step_khz); chan &= 0x3FFF; // 只保留低 14 位 si4703_read_all(); si4703_regs[0x03] = 0x8000 | chan; // bit15 = TUNE,启动调谐 si4703_write_all(); // 等待 STC 置位,STC 在 STATUS 寄存器 bit14 do { si4703_read_all(); } while ((si4703_regs[0x0A] & 0x4000) == 0); // 清掉 TUNE 位,避免下一次调台时残留上次状态 si4703_regs[0x03] &= ~0x8000; si4703_write_all(); }因为 I2C 每次返回 16 个寄存器,所以读一次就能拿到 STATUS,不用单独访问状态寄存器。这个清 TUNE 位的动作很多例程不做,但我建议做掉,因为下一次调台写入 CHANNEL 时,如果 TUNE 一直是 1,有些批次的 Si4703 会自动重新调一次当前频率,造成一次多余的 STC 跳变。
在实际板子上,100ms 超时可能不够。如果 RCLK 起振慢,STC 置位可能拖到 200ms 以上。你可以把HAL_Delay放到循环里做个看门狗式超时,避免芯片异常时主程序卡死在 do-while 里。
4.3 自动搜台:用一次 SEEK 命令遍历频段
自动搜台本质是让芯片自己从当前频率向上或向下扫描,遇到信号强度超过 SEEKTH 的频点就停下。下面代码给出逻辑骨架,具体位值需要对齐你的数据手册:
// POWERCFG 相关的位定义示例,按数据手册修正 #define PWRCFG_ENABLE 0x0001 // 使能芯片 #define PWRCFG_SEEK 0x0002 // 发起一次搜索 #define PWRCFG_SEEKUP 0x0004 // 搜索方向向上 int si4703_seek(int seek_up) { uint16_t timeout = 300; si4703_read_all(); si4703_regs[0x02] |= PWRCFG_ENABLE; if (seek_up) si4703_regs[0x02] |= PWRCFG_SEEK | PWRCFG_SEEKUP; else si4703_regs[0x02] |= PWRCFG_SEEK; si4703_write_all(); while (timeout--) { si4703_read_all(); if (si4703_regs[0x0A] & 0x4000) { // STC=1,搜索结束 si4703_read_all(); if (si4703_regs[0x0E] & 0x0001) // VALID 位:停在有效台上 return 1; else return 0; // 找到但信号不可用 } HAL_Delay(1); } return -1; // 超时 }搜索结束后,CHANNEL 寄存器里会自动更新成新频率,你不需要手动写。如果返回值是 0,表示没找到有效台,可以把 SEEKTH 调低一点再试一次。
RSSI 的读取方法很简单,它在 RADIOC 寄存器的高 4 位:
uint16_t si4703_get_rssi(void) { si4703_read_all(); return (si4703_regs[0x0E] >> 12) & 0x0F; }这个 4bit 粗读足够用来判断“有没有台”。如果要做精准场强显示,需要关闭 AGC 后再读,那是量产阶段的事。调试阶段用这个值配合串口打印,能直观看到当前频点信号强度是多少格。
5. Si4703 实操避坑:5 个高频故障的现象、原因和解决
5.1 现象:I2C 读回全是 0xFF,芯片像不存在
I2C 总线上没有 ACK 响应,读回的数据全是 0xFF,这是最高频的故障。原因有三个:一是 RST 引脚悬空或接法不对,芯片没进入 I2C 模式;二是 I2C 地址不对,Si4703 的 7 位地址由 SEN 引脚决定,市场上多数模块把地址做成 0x10,但有些批次是 0x11;三是 SDA/SCL 没有外部上拉。
解决步骤:先用万用表量 RST 引脚电平,确认能够从低拉高。然后读模块丝印或原理图确认 SEN 接法。最后用 100kHz 的 I2C 速度去扫描总线,通杀一切时序问题。这个坑和 STM32 I2C 外设本身也有关,如果总线处于 Busy 状态,可以先重新初始化 GPIO 为开漏输出,再恢复总线。
5.2 现象:寄存器能读能写,但耳机里完全无声
能读到寄存器,说明 I2C 已经通了,但没声音,问题基本在音频链路。先看 LOUT 和 ROUT 是否通过电容接到了耳机座,不能直接接一个几十欧的喇叭,因为芯片音频驱动能力很弱。然后检查 POWERCFG 里的 ENABLE 有没有保持为 1,有些库在搜台时会不小心把 DISABLE 位置 1。最后看软静音和音量寄存器,音量不是越大越好,有些值会触发芯片的过载保护。
一个很实用的排除法:用手指碰一下天线焊盘,如果耳机里有明显的“滋滋”声,说明音频链路是通的,问题出在天线或信号强度;如果什么声音都没有,问题在 LOUT/ROUT 到耳机座这段电路。
5.3 现象:搜台搜过不停,或者停在噪声上
自动搜台的目的是停在强台上,但如果你把 SEEKTH 调得太低,芯片会把噪声当信号。典型现象是搜台过程一直不停,偶尔停在某个频率,但听起来只有“沙沙”声。
解决办法是提高 SEEKTH。SYSCONFIG2 的 SEEKTH 默认 0x20,我一般从 0x28 到 0x30 之间选。如果你所在区域电台偏远,信号普遍较弱,阈值可以降到 0x18,但要做好停在噪声台的心理准备。另一个原因是 BAND 和 SPACE 没设置对,比如把 SPACE 设成 50kHz,但当地广播都是 200kHz 步进,搜台会在一堆谐波上停下来。
5.4 现象:STC 位一直不置位,调台卡死
STC 置位的前提是芯片内部完成频率锁定,而这个锁定需要参考时钟。如果 RCLK 引脚没有接入 32.768kHz 晶振,或者晶振虚焊,STC 永远不动。用示波器量 RCLK 引脚,应该能看到正弦或方波,频率偏差应小于 20ppm。
还有一种可能是你清了 TUNE 位太早。有些驱动在写入 CHANNEL 后马上又写一次清零,芯片还没来得及开始调谐就被打断了。正确做法是等 STC 置位后再清 TUNE,我代码里也是这个顺序。如果你发现 STC 置位很快但频率不对,多半是 RCLK 频偏太大导致的。
5.5 现象:HAL_Delay 卡死,主循环进不去
这个问题我遇到过好几次,现象是程序复位后过一会儿就不动了,调试器暂停后发现卡在HAL_Delay里。原因通常是 SysTick 中断被别的优先级更高的中断阻塞,或者 I2C 中断配置不正确。
在 Si4703 驱动里,HAL_Delay被频繁调用,复位和搜台都靠它。如果工程里有别的外设中断被配成抢占优先级 0,SysTick 和它们发生嵌套时,HAL_Delay就会等不到 tick。解决方法是把 SysTick 中断优先级降到 1 或 2,或者干脆把 Si4703 的复位延迟改成自旋延时函数,不依赖 HAL_Delay。还有一个更隐蔽的原因:CubeMX 生成工程时把 I2C 配成了中断模式,但你没写中断回调函数,I2C 事件一直被挂起,总线状态异常。我建议调试阶段全部用阻塞式 I2C,跑通再改 DMA。
6. 进阶验证:串口打印 RSSI 和寄存器快照
6.1 串口输出寄存器快照,把黑盒变成白盒
收音机这种功能,信号强度、状态字、寄存器变化都藏在芯片内部,不打印出来很难排错。下面这段代码用串口把 16 个寄存器全部打出来,你运行之后直接看到 STC、VALID 和 RSSI 的变化,比盲改参数靠谱得多。
void si4703_dump_regs(UART_HandleTypeDef *huart) { char line[64]; si4703_read_all(); for (int i = 0; i < 16; i++) { sprintf(line, "R[0x%02X]=0x%04X\r\n", i, si4703_regs[i]); HAL_UART_Transmit(huart, (uint8_t*)line, strlen(line), 100); } }串口打印和 2402 液晶屏二选一即可。如果你之前折腾过 ILI9341 这类屏,就会知道读 ID 读回来 0xA1A1 和这里 I2C 读回 0xFF 是同一种问题:初始化时序不对,外设根本没进入预期状态。用串口就不需要靠屏幕判断芯片状态了。
6.2 把参数封装成结构体,方便后续扩展
我不建议在主函数里直接操作si4703_regs[0x03]这种裸索引,调一次台就要记一次地址,很容易写错。更好的做法是把频率、RSSI、音量、搜台状态放进一个结构体,表层函数只操作结构体,底层函数再映射到寄存器。
typedef struct { uint16_t freq_khz; // 当前频率,单位 kHz uint8_t rssi; // 当前信号强度 0~15 uint8_t volume; // 音量 0~15 int seek_stopped; // 搜台是否找到台 uint16_t status; // STATUS 寄存器镜像 } si4703_app_t;每次调台后更新这个结构体,串口打印也只打印结构体成员。这样以后加 RDS 解码、加定时自动搜台、加音量记忆,都不用改动底层 I2C 逻辑。
6.3 我的一个习惯:每次调完都做一次读回对比
写寄存器前先保存镜像,写完后读回来和镜像做逐位对比,一旦发现写入失败,至少能定位到第几次写入翻车。这个习惯救过我很多次,尤其是硬件上 I2C 上拉电阻虚焊的时候,第一次写入成功,第二次却失败,打印结果一眼就能看出来。
另外我每块 Si4703 板子的耳机座地线都会串联 1kΩ 电阻后再接电源地,这样能减少插拔耳机时的电流声,代价是可能让天线地不够实,你根据自己板子权衡。别忘了去加重和软静音这两个参数,它们决定了声音能不能长期听,不只是在示波器上“能响”。希望帮到你。
本文还有配套的精品资源,点击获取