
简介基于51单片机的RDA5807FP数控收音机完整工程资料面向电子爱好者、嵌入式开发者及单片机课程设计学生解决传统模拟调谐收音机操作繁琐、难以精确选台的问题。项目以低功耗FM收音芯片RDA5807FP为射频核心由51单片机统一调度实现自动搜台、数字音量调节、电台预存、频率显示与LCD人机交互符合当前收音机向数字化、智能化演进的设计思路。压缩包共70个文件、大小约106KB包含14个C源码模块、15个头文件、编译输出的hex固件、uvproj工程、A51启动文件以及lst/obj/m51等中间产物既便于查看编译流程和程序结构也支持直接烧录到开发板验证效果。目前已有1975人学习下载。代码覆盖主控逻辑、I2C通信、按键扫描去抖、Nokia5110液晶显示、DS1302实时时钟、DS18B20温度采集等完整功能模块模块划分清晰既适合作为单片机综合实训案例也可在此基础上继续扩展新功能为二次开发留出充足空间。1. 数控收音机不等于“用按键换旋钮”RDA5807FP 工程的真正价值手头这份rda5807fp数控收音机.rar解压后是一套完整的 Keil 工程芯片选型是 RDA5807FP 加 51 单片机外挂 DS1302 时钟、DS18B20 测温、24C02 存储和 Nokia 5110 屏。第一眼看去像是课程设计惯用的“大杂烩”但把文件列表逐行过一遍后发现它其实把数控收音机的几个典型痛点都覆盖了FM 频点由数字寄存器写入而非可变电容、音量与静音状态可编程控制、电台预设依赖 24C02 断电保存、交互反馈走 5110 液晶。这套硬件组合在低成本便携收音机、桌面小设备、教学演示板里非常常见也是理解“模拟调谐到数字调谐”转型的一个极佳样本。很多人误以为数控收音机就是把旋钮替换成按键实际上关键差异在于频率合成与状态保持。RDA5807FP 内部集成调谐器、解码器和音频放大器外部 MCU 只通过 I2C 写配置寄存器。这个项目对两类人最有价值一是想把 51 单片机从流水灯带向真实无线通信场景的开发者二是要快速移植 RDA5807 驱动到其他 MCU 平台STM32、ESP32、GD32的工程师。后者可以从工程里的radio.c、iic.c抽出驱动骨架改一下平台层就能复用。2. RDA5807FP 寄存器配置从软复位到 FM 频点写入的完整时序2.1 芯片与 MCU 的接线和控制模型下载解压后先看原理图部分工程内若附带 PDF 或截图可直接对照引脚。RDA5807FP 是 20 引脚封装MCU 侧真正需要操作的信号只有三根SCL、SDA、以及可选的 RST部分模块直接上拉不接 MCU。值得注意的是芯片支持 I2C 和 SPI 两种控制方式而 51 工程里普遍用 GPIO 模拟 I2C这不是因为芯片不支持硬件 I2C而是因为 51 内核如 STC89C52的硬件 I2C 模块并不普及大多数开发板默认用普通 IO 口模拟更稳妥。// iic.h 中通常做这样的引脚映射 sbit SCL P2^0; sbit SDA P2^1; #define IIC_SCL_H SCL 1 #define IIC_SCL_L SCL 0 #define IIC_SDA_H SDA 1 #define IIC_SDA_L SDA 0 #define IIC_SDA_R SDA模拟 I2C 的优点在于时序完全可控时钟频率可以压到 100kHz 以下对长导线和电平转换电路更友好缺点是 CPU 占用高。这里的做法是把底层的起始、停止、发送字节、接收字节封装成iic.c上层radio.c只关心“往某寄存器写 16 位数据”或“从某寄存器读 16 位”。2.2 寄存器地址与软复位时序RDA5807FP 的寄存器是 16 位宽地址从 0x00 到 0x0F。核心操作集中在几个地址寄存器地址作用关键位说明0x02软复位、频率高字节bit15 软复位bit14 向上搜台bit6:0 为 CH[13:8]0x03频率低字节CH[7:0]频率字 目标频点与步进换算0x04开关控制bit1 解除软复位bit4 立体声/单声道0x05音量控制bit6 静音bit5:2 音量等级0-150x0A状态读取bit5 搜台完成标志bit4 搜台成功标志bit2:0 为 CH[13:11] 的高位初始化顺序不能乱先写 0x02 的 bit15 触发软复位再通过 0x04 解除复位最后设置频率和音量。如果先写频率再解除复位芯片处于复位态时 I2C 虽然应答但寄存器内容会被复位覆盖表现为搜不到台或输出静音。void RDA5807_Init(void) { // 1. 软复位0x02 寄存器 bit15 置 1 RDA5807_WriteReg(0x02, 0xC001); DelayMs(10); // 2. 解除复位0x04 寄存器 bit1 置 1此时芯片开始工作 RDA5807_WriteReg(0x04, 0x0009); DelayMs(100); // 3. 默认音量0x05 寄存器bit6 静音位清 0bit5:2 音量设为 8 RDA5807_WriteReg(0x05, 0xC080); }这段代码有两点值得展开。第一0xC001中高字节0xC0包含了软复位位和部分控制位不同版本的芯片对保留位的要求略有差异工程里写0xC001是常见保险值如果你用的是 RDA5807M 或其他批次改为0x0001同样可以触发复位。第二0x04写入0x0009的 bit0 用于启用内部基准振荡器bit1 解除软复位。数据手册上 bit3 控制 GPIO1 输出这里取 0 不影响功能。2.3 FM 频点换算公式与写入实现频率写入是整个驱动的核心。RDA5807FP 的频点不是直接写 MHz 数值而是按步进换算成 14 位通道号 CH。芯片默认步进 50kHz标准 FM 频段从 87.0MHz 起跳// 换算公式CH (目标频率MHz - 87.0) / 0.05 // 例如 100.0MHz (100.0 - 87.0) / 0.05 260 0x0104 unsigned int RDA5807_FreqToWord(float freq_mhz) { unsigned int ch; if (freq_mhz 87.0) freq_mhz 87.0; if (freq_mhz 108.0) freq_mhz 108.0; ch (unsigned int)((freq_mhz - 87.0) * 20.0 0.5); return ch; } void RDA5807_SetFreq(float freq_mhz) { unsigned int word RDA5807_FreqToWord(freq_mhz); // CH[13:8] 写入 0x02 低字节CH[7:0] 写入 0x03 高字节 RDA5807_WriteReg(0x02, (word 8) 0x3F); RDA5807_WriteReg(0x03, word 0xFF); }(freq_mhz - 87.0) * 20.0就是(freq - 87) / 0.05的变形乘以 20 等价于除以 0.05避免了浮点除法带来的精度损失。0.5用于四舍五入防止 99.95MHz 这类边界值换算后因为浮点误差被截断成 257 而不是 258。这里有一个很多初学者会踩的坑写入 0x02 寄存器时如果直接用word 8会把 bit6、bit7 也带入寄存器干扰搜台启动位和频段选择位。所以代码里加 0x3F掩码只保留 CH[13:8]。网上不少精简版驱动不写掩码短时间能工作但当你要实现自动搜台bit14 置 1时就会发现频点跳到了错误的半区这正是掩码缺失的隐患。2.4 音量与静音控制的寄存器位音量部分相对简单但注意与频率写入的状态依赖关系void RDA5807_SetVolume(unsigned char vol) { unsigned int reg; if (vol 15) vol 15; // 先读出当前 0x05 寄存器保留 bit7、bit6 的状态 reg RDA5807_ReadReg(0x05); reg 0xFFC3; // 清掉 bit5:2 reg | (vol 2); // 音量写入 bit5:2 RDA5807_WriteReg(0x05, reg); } void RDA5807_SetMute(unsigned char mute) { unsigned int reg RDA5807_ReadReg(0x05); if (mute) reg | (1 6); else reg ~(1 6); RDA5807_WriteReg(0x05, reg); }不要直接RDA5807_WriteReg(0x05, vol 2)因为 0x05 的高位还控制着频段选择、立体声模式以及 LO 端口注入方式。先读后写、只改目标位段是 I2C 设备驱动的基本素养这个习惯从 51 时代就要养成。3. 51 单片机侧的按键扫描与状态机音量、频道、预设台的组织方式3.1 按键矩阵扫描与消抖工程里的key_scan.c和Key_Handle.c分别负责底层扫描和业务处理。典型 4x4 矩阵键盘占用 P1 口扫描原理是逐行拉低电平读取列输入判断哪个按键被按下。裸机环境下最麻烦的是消抖和响应速度的平衡。#define KEY_MATRIX P1 // 每 10ms 调用一次返回键值0 表示无按键 unsigned char Key_Scan(void) { unsigned char row, col, key_val 0; for (row 0; row 4; row) { KEY_MATRIX ~(0x01 row); // 当前行输出低电平 for (col 0; col 4; col) { if (!(KEY_MATRIX (0x08 col))) { // 列检测 DelayMs(10); // 软件消抖 while (!(KEY_MATRIX (0x08 col))); // 等待释放 key_val row * 4 col 1; return key_val; } } } return 0; }这段代码的缺陷也很明显while等待释放是阻塞式的如果用户按住按键不放LCD 刷新、温度采集全部停摆。更好的做法是把消抖做成状态机检测到按下后标记“待确认”下一次扫描仍为按下才判定有效释放时不等待。工程用这种简单写法是因为演示场景不需要高并发响应但你自己做产品时应该升级为“按下沿触发 释放不阻塞”的模式。3.2 功能状态机设计从裸奔按键到按逻辑分层有了键值之后业务层需要一个状态机来决定“这个键在当前界面下做什么”。工程的做法是把界面分为频率模式、音量模式、预设台模式、时间设置模式。typedef enum { MODE_FREQ 0, MODE_VOL, MODE_PRESET, MODE_TIME } SYS_MODE; SYS_MODE current_mode MODE_FREQ; void Key_Handle(unsigned char key) { switch (current_mode) { case MODE_FREQ: if (key KEY_UP) RDA5807_SetFreq(current_freq 0.1); if (key KEY_DOWN) RDA5807_SetFreq(current_freq - 0.1); if (key KEY_MODE) current_mode MODE_VOL; break; case MODE_VOL: if (key KEY_UP) RDA5807_SetVolume(current_vol); if (key KEY_DOWN) RDA5807_SetVolume(--current_vol); if (key KEY_MODE) current_mode MODE_PRESET; break; // PRESET/TIME 省略 } }状态机本身不复杂真正要关注的是按键“长按连加”和“短按步进”的区分。短按一次加 0.1MHz长按 1 秒后进入自动连加模式连加速度要平滑不跳变。实现上依赖定时器中断计时比如每 10ms 扫描一次长按计数超过 100 就触发一次连续步进并重置计数。工程里Time_Handle.c就是为此服务的它会维护一个全局 tick按键处理里通过 tick 差来判断按下的持续时间。3.3 预设电台与 24C02 存储联动预设台功能是把当前频率写入 24C02下次上电恢复。这涉及两个问题存储位置分配和校验。工程里 24C02 是 256 字节建议前 10 字节存预设频道编号第 11 字节存音量最后一个字节存校验和。#define PRESET_BASE_ADDR 0x00 #define VOLUME_ADDR 0x10 #define CHECKSUM_ADDR 0x1F void Save_Preset(unsigned char index, unsigned int freq_word) { // 24C02 按字节写先写高字节再写低字节 AT24C02_WriteByte(PRESET_BASE_ADDR index * 2, (freq_word 8) 0xFF); AT24C02_WriteByte(PRESET_BASE_ADDR index * 2 1, freq_word 0xFF); } void Save_AllConfig(void) { unsigned char sum 0; AT24C02_WriteByte(VOLUME_ADDR, current_vol); // 简单校验把所有预设台高字节异或 for (i 0; i 10; i) sum ^ AT24C02_ReadByte(PRESET_BASE_ADDR i * 2); AT24C02_WriteByte(CHECKSUM_ADDR, sum); }上电恢复时先读出所有预设台计算异或和与CHECKSUM_ADDR中保存的值比对一致则加载不一致则使用默认频率 87.5MHz。异或校验虽然简单但能挡住大部分“写到一半掉电”导致的数据损坏。工程里可能有更复杂的校验策略比如双区备份如果解压后看到类似24c02.c中有冗余写入逻辑优先保留。3.4 定时器中断与主循环的时间分片51 单片机跑这种多任务 UI常见做法是用定时器 0 产生 1ms 或 10ms 中断作为心跳主循环只做事件分发。工程里的System_Init.c负责配置定时器和中断优先级Time_Handle.c维护系统时钟。void Timer0_ISR(void) interrupt 1 { static unsigned int counter 0; TH0 0xFC; // 1ms 重装初值12MHz 晶振 TL0 0x67; counter; if (counter % 10 0) { key_tick; // 按键扫描节拍 lcd_refresh_tick; // LCD 刷新标志 } if (counter % 1000 0) { ds18b20_tick; // 温度采集节拍 } }主循环则是轮询这些 tick 标志位非阻塞地执行对应模块。如果工程里看到if (flag_10ms) { flag_10ms 0; Key_Scan(); }这类代码说明作者采用了事件标志位模式这种结构在单个 51 上挂四个外设时是保证响应速度的关键。4. DS1302、DS18B20、24C02 与 Nokia 5110数控收音机的外围协同4.1 外设接口分配与电平匹配策略这个工程把 51 单片机的外设资源几乎用满P1 口给矩阵键盘P2 口给 I2CRDA5807FP 和 24C02 共用P3 口给 DS1302、DS18B20 和 Nokia 5110 的 SPI。先梳理一下引脚分配外设接口类型引脚占用关键点RDA5807FPI2CP2.0 SCL, P2.1 SDA地址 0x107 位24C02I2C同上总线复用地址 0xA08 位写地址DS1302自定义三线P3.0 CE, P3.1 SCLK, P3.2 IO数据在上升沿写入DS18B20单总线P3.3 DQ需 4.7k 上拉电阻Nokia 5110SPI 模拟P3.4 RST, P3.5 CE, P3.6 DC, P3.7 DIN, P2.2 SCLK对比度靠指令设置RDA5807FP 和 24C02 挂在同一条 I2C 总线上很常见但要注意两点。第一两者的 7 位地址不同总线仲裁没问题第二RDA5807FP 的 SCL/SDA 不需要额外上拉芯片内部已有弱上拉而 24C02 的 WP 写保护脚必须接地或接 MCU 控制否则写入无效。很多“能读不能写”的问题就出在 WP 悬空。4.2 Nokia 5110 的中文字库显示机制工程里nokia_5110.c和font.h、Dis_Chinese.h构成了显示层。5110 屏是 84x48 像素英文用 6x8 点阵中文要显示 12x12 点阵需要通过字模软件生成数组。// 显示一个 6x8 ASCII 字符 void LCD_WriteChar(unsigned char row, unsigned char col, char c) { unsigned char i; LCD_SetPos(col * 6, row * 8); // 列坐标换算成像素 for (i 0; i 6; i) { LCD_WriteData(Font6x8[c - ][i]); } } // 显示中文每个字 12x12占两行 void LCD_WriteChinese(unsigned char row, unsigned char col, unsigned char index) { unsigned char i; unsigned char const *p Chinese_12x12[index]; LCD_SetPos(col * 12, row * 12); for (i 0; i 12; i) { LCD_WriteData(p[i]); // 上半部分 } LCD_SetPos(col * 12, row * 12 8); for (i 0; i 12; i) { LCD_WriteData(p[i 12]); // 下半部分 } }工程里的Dis_Chinese.h应该内置了“频率”“音量”“时间”“温度”等常用词的字模。如果你要扩展菜单用取模软件生成 12x12 点阵按列行式先列后行排布然后把数组添加到Dis_Chinese.h末尾即可。字号换算是个易错点6x8 字体的行间距是 8 像素5110 屏幕 48 像素高正好 6 行12x12 字体每行占 12 像素屏幕只能放 4 行中文。4.3 DS18B20 温度采集与 LCD 联动刷新DS18B20 是本工程里比较讨巧的一个功能点收音机本身和温度没有直接关系但把它做到 LCD 界面上可以让设备看起来更像一个多功能桌面信息站。DS18B20 的驱动注意 51 单片机的时序是微秒级不能用DelayMs这类毫秒延时函数混过去。unsigned char DS18B20_ReadByte(void) { unsigned char i, byte 0; for (i 0; i 8; i) { byte 1; DQ 0; _nop_(); DQ 1; _nop_(); if (DQ) byte | 0x80; DelayUs(60); } return byte; }读时序中DQ 1释放总线后要立即读数据中间只能有 1-2 个_nop_()的延迟不能调用DelayUs(10)。如果波形偏离数据手册要求读出的温度会间歇性变成 85℃ 或 0℃。另外DS18B20 可以切换 9-12 位精度默认 12 位转换时间 750ms如果 LCD 上报的温度刷新速度太慢可以在DS18B20_Init里写入配置寄存器改为 10 位精度转换时间降到 187.5ms代价是分辨率从 0.0625℃ 降到 0.25℃。4.4 DS1302 时间基准与掉电走时DS1302 提供时间基准液晶界面显示当前时钟。写时间时要先关闭写保护0x8E 寄存器的 bit7 清 0写完再打开。这里有个常见误区DS1302 的时钟芯片在主控掉电后依靠 3V 纽扣电池继续走时但在 Proteus 仿真中如果不放置电池模型时间不会走动。uchar Read_Ds1302_Byte(uchar addr) { uchar dat 0; CE 1; // 使能 Write_Ds1302_Byte(addr | 0x01); // 地址最低位为 1 表示读 for (i 0; i 8; i) { dat 1; if (IO) dat | 0x80; // 上升沿采样 SCLK 1; SCLK 0; } CE 0; return dat; }注意 DS1302 读写的字节序低位在前这与 I2C 的高位在前完全不同。初次移植驱动时最容易把这里搞反导致读出的时间像乱码一样跳变。判断方法很简单秒寄存器读出后十进制显示的个位如果只在 0-9 范围但十位异常就是 BCD 转换问题如果整个数值规律性偏移多半是移位方向错了。5. I2C 总线锁死与寄存器回读RDA5807FP 调试中的几个关键动作RDA5807FP 这个芯片调试到最后真正劝退人的往往不是频率换算而是 I2C 通信本身。51 的 GPIO 模拟 I2C 在工程上选了推挽输出但 RDA5807FP 的 SDA 是开漏结构。如果代码里写 SDA 输出高电平时直接SDA 1一旦外设拉低总线GPIO 和芯片内部驱动就形成灌电流轻则读数异常重则锁死总线。建议改法把 SDA 引脚模式切换成输入模式来读取需要拉低时才输出低电平。// 释放 SDA让总线自己上去 #define IIC_SDA_RELEASE() { P2M1 | 0x02; P2M0 | 0x02; } // STC89 系列设为高阻输入 #define IIC_SDA_LOW() { P2M1 ~0x02; P2M0 ~0x02; SDA 0; }还有一个更实用的排错手段寄存器回读验证。写完频率后不要急着听声音把 0x0A 寄存器读回来检查 bit5 的搜台完成标志和 bits 2:0 的高位 CH 值。unsigned int RDA5807_ReadFreqWord(void) { unsigned int reg02, reg03, reg0A; unsigned int ch 0; reg0A RDA5807_ReadReg(0x0A); reg02 RDA5807_ReadReg(0x02); reg03 RDA5807_ReadReg(0x03); // 从 0x0A 的 bits 2:0 取出 CH[13:11] ch (reg0A 0x0007) 11; ch | (reg02 0x003F) 8; ch | (reg03 0x00FF); return ch; }如果你写入 100.0MHz 后回读的 word 是 2600x0104说明寄存器链路正常如果回读始终是 0 或最大值问题不在频率计算而在 I2C 时序或地址配置。另外可以善用 Proteus 仿真先在 Proteus 8 里搭建 51 RDA5807FP 的虚拟模型将 I2C 时序信号接入虚拟示波器对比数据手册确认起始条件、ACK 位和 16 位数据帧格式无误后再烧录到实体板可以避免反复焊接排查。调速方面如果发现 24C02 和 RDA5807FP 在同一总线上互相干扰可以尝试把总线时钟从 50kHz 提高到 100kHz 或降低到 20kHz观察两边的应答时序。RDA5807FP 支持到 400kHz但 51 的 GPIO 翻转速度有限STC89C52 在 12MHz 晶振下模拟 I2C实际上限通常在 50kHz 以内。最后检查一下iic.c中发送字节后是否等待 ACK如果发现读回来的 ACK 位总是 1无应答优先测量 SCL/SDA 引脚电平确认焊点和上拉电阻是否到位。本文还有配套的精品资源点击获取