简介:这是一份面向NEC 78K系列单片机开发者的按摩椅控制程序源码包,以日本FAMILY W-1按摩椅为对象,主控芯片为NEC UPD784218。资源围绕按摩强度调节、模式选择、时间设定等实际功能,覆盖电机驱动、VFD真空荧光显示、按键扫描、定时中断等嵌入式开发核心环节,适合嵌入式初学者或家电控制器开发人员研究参考。
压缩包内共138个文件,大小仅421KB,包含22个C源文件、19个H头文件、34个BAK备份、19个REL可重定位文件,以及ASM汇编、HEX固件等类型,便于对照源码、生成文件与最终烧录镜像进行学习。通过系统初始化、VFD显示驱动、按键处理等模块的工程组织,可以直观了解整机控制流程与软硬件接口关系。
资源当前已有125人学习下载。对想要了解NEC单片机在家电产品中的真实工程结构、或需要参考C与汇编混合编程和VFD显示驱动的开发者,这是一份体量轻但信息密度较高的参考材料。
1. 按摩椅程序里的 NEC 与单片机
按摩椅的遥控和电控有两套东西配合:红外接收端用 NEC 协议,38kHz 载波,解码后得到 32 位数据帧,帧里带地址和命令反码;单片机端则把帧翻译成揉捏电机、敲打电机、气囊电磁阀的动作。拿到带“nec”标签的按摩椅程序工程,先读红外解码部分比读电机控制更有价值,因为后面所有映射和状态机都在消费解码输出,这一层出错,动作全是垃圾输入。
普通家电按一下动一下,按摩椅的一个动作要保持几十秒再换下一个,核心是状态机而不是按键分发。下面讲的 NEC 解码、电机控制、看门狗和现场排查,对做按摩椅、理疗床、康复器械的固件工程师以及毕设选“按摩椅控制系统”的学生都适用;先找到执行机构,再跳到对应章节。
2. 单片机 NEC 红外解码:从时间片到位映射
2.1 NEC 协议在按摩椅遥控器里的时间参数
NEC 协议在单片机端看到的是一串宽窄不同的脉冲。引导码是 9ms 的 38kHz 载波突发加 4.5ms 空闲,数据位是约 560µs 的载波突发加一段空闲,空闲长度在 1690µs 左右是逻辑 1,560µs 左右是逻辑 0。对下降沿中断来说,从一次下降沿到下一次下降沿,逻辑 1 约为 2.25ms,逻辑 0 约为 1.125ms,差了一倍,阈值取 1.5ms 时基本不挑器件。
按摩椅遥控器按键多,常见做法是走扩展 NEC 或自定义命令段,把地址固定成厂内编号,命令码对应到具体按键。标准 NEC 的数据帧一共 32 位,按 LSB 在前发送:8 位地址、8 位地址反码、8 位命令、8 位命令反码。只要接收端把地址反码和命令反码都校验,基本能挡住同频段其它遥控器的偶然干扰。
| NEC 帧参数 | 数值 | 解码用途 |
|---|---|---|
| 载波频率 | 38kHz | 一体化接收头解调 |
| 引导码 | 9ms 载波 + 4.5ms 空闲 | 判定帧头 |
| 逻辑 0 | 560µs 载波 + 560µs 空闲 | 下降沿间隔约 1.125ms |
| 逻辑 1 | 560µs 载波 + 1690µs 空闲 | 下降沿间隔约 2.25ms |
| 数据帧总长 | 32 位,LSB 在前 | 地址+反码+命令+反码 |
| 重复码 | 9ms 载波 + 2.25ms 空闲 | 长按自动发送 |
提示:一体化红外接收头收到载波时输出低电平、空闲输出高电平,单片机看到的是反相波形,外部中断建议配下降沿或双边沿触发。
2.2 用定时器捕获实现 NEC 解码的完整代码
在 51 内核上做 NEC 解码,我一般不用定时中断去轮询电平,而是让一个 16 位定时器自由跑,外部中断每次触发时读一次计数器,算差值。这样中断里只有几次减法和比较,时间上能控制在几微秒内,解码过程中主循环还能同时处理电机任务。
// NEC 解码:适配 STC15W / 标准 51,12MHz 主频 // 定时器 0 自由计数,INT0 下降沿触发 sbit IR_IN = P3^2; // 接收头 OUT 接 P3.2 volatile unsigned int last_tick = 0; volatile unsigned char bit_cnt = 0; volatile unsigned char frame[4] = {0}; volatile unsigned char frame_ok = 0; void ir_int0_isr(void) interrupt 0 { unsigned int now = (TH0 << 8) | TL0; unsigned int diff = now - last_tick; last_tick = now; if (diff > 12000) { // 两帧间的空白,清空缓冲 bit_cnt = 0; return; } if (diff > 7000) { // 引导码:9ms + 4.5ms 或重复码 bit_cnt = 0; return; } if (diff > 400 && diff < 5000 && bit_cnt < 32) { if (diff > 1500) { // 2.25ms 为逻辑 1 frame[bit_cnt >> 3] |= (1u << (bit_cnt & 7)); } bit_cnt++; } if (bit_cnt == 32) { frame_ok = 1; bit_cnt = 0; } }定时器初始化只要保证TH0和TL0在启动后不再被主程序赋初值,让它一直自由运行即可。diff = now - last_tick利用无符号整数回绕的特性,即使两次中断之间发生了定时器溢出,也能得到正确的间隔,不需要额外处理溢出标志。这个解码器里没有用中断屏蔽,中断函数里没有阻塞操作,NEC 一位数据的间隔在 1.1ms 以上,单片机完全来得及响应。
阈值选择上,引导码判定取了 7000µs,这是因为它同时覆盖数据帧引导码 13.5ms 和重复码 11.25ms 两种间隔。数据位的下限取 400µs,是为了挡住边沿抖动或极窄的杂波。若实际遥控器晶振偏差大,位宽漂移超过 10%,把 1500µs 阈值调整到 1300µs 即可,代码逻辑不用改。
2.3 从 NEC 功能码到按摩椅按键动作的映射表
解码器输出裸的 4 字节帧,按键语义要单独建一层。按摩椅按键通常包括电源、自动、揉捏、敲打、推拿、气囊、加热、强度加、强度减和停止。项目上做协议定义时,我会先把键值表写进需求文档,固件里也只在唯一的地方维护这张表。
| 按键名 | 命令码 | 动作 |
|---|---|---|
| 电源 | 0x45 | 待机/运行切换 |
| 自动 | 0x46 | 进入自动按摩程序 |
| 揉捏 | 0x47 | 揉捏电机启动 |
| 敲打 | 0x44 | 敲打电机启动 |
| 气囊 | 0x40 | 气泵和电磁阀循环 |
| 加热 | 0x43 | 加热继电器吸合 |
| 停止 | 0x42 | 全部电机停转并回位 |
这里的 0x45、0x46 不是 NEC 协议的强制值,是按摩椅行业内比较常见的一组遥控码。不同厂家、不同批次遥控器的码表可能完全不同,所以项目开始时必须先烧一个打印原始帧的测试固件,把每个键对应的字节记录下来再填表,不要直接套用别人工程里的键值。
2.4 NEC 帧校验与主循环取帧
帧里第二个字节是地址反码、第四个字节是命令反码,校验规则很简单:frame[1]应等于~frame[0],frame[3]应等于~frame[2]。还有一部分方案会把整个数据帧取反发送,在初始化字节不为 0 时两者有对应差异,程序里统一按“校验失败就丢帧”处理。
#define IR_ADDR 0x00 // 按摩椅本机接收地址 void process_nec_cmd(void) { unsigned char tmp[4]; if (!frame_ok) return; EA = 0; // 关中断拷贝,防 32 位帧被中途更新 tmp[0] = frame[0]; tmp[1] = frame[1]; tmp[2] = frame[2]; tmp[3] = frame[3]; frame_ok = 0; EA = 1; if (tmp[0] != (unsigned char)~tmp[1] || tmp[2] != (unsigned char)~tmp[3]) { return; // 反码不匹配,丢弃 } if (tmp[0] != IR_ADDR) return; // 地址不是本机,丢弃 dispatch_key(tmp[2]); // 只把命令码交给业务层 }关中断拷贝是因为frame[4]是多字节组合,主循环读它时,外部中断可能正好写入新帧,造成前一半是旧帧、后一半是新帧的错位数据。校验在这里就是最后一道闸。按帧周期 40ms 到 110ms 统计,NEC 遥控器按住不松会反复发重复码。按摩椅几乎每个按键都是自锁式,长按没有额外语义,所以解码器直接把重复码当作噪声丢弃;如果将来要做长按连续调档,在收到重复码时记住当前命令值再走一次dispatch_key即可。
3. 按摩椅电机控制:驱动选型、PWM 参数与状态机编排
3.1 按摩机芯的执行机构与驱动方案
按摩椅的执行器分四类:揉捏电机、敲打电机、行走电机,以及气泵加电磁阀的气囊系统。揉捏和敲打电机常是 24V 直流有刷电机,要正反转和调速,驱动用 H 桥。气泵功率大但只需要开关,用继电器控制。电磁阀数量多、电流小,用 ULN2803 这类达林顿阵列最省事,一块芯片能带七路,正好覆盖常见的气囊路数。
| 执行机构 | 电压 | 驱动 | 控制方式 | 关键参数 |
|---|---|---|---|---|
| 揉捏电机 | 24V DC | H 桥 | PWM + 方向 | 连续运行 |
| 敲打电机 | 24V DC | H 桥 | PWM + 方向 | 频繁启停 |
| 行走电机 | 24V DC | H 桥 | 方向 + 限位 | 两端微动开关 |
| 气泵 | 12V/24V | 继电器 | 开/关 | 大电流浪涌 |
| 电磁阀 | 12V | ULN2803 | 开/关 | 7 路 |
| 加热丝 | 220V | 继电器 | 开/关 | 需要安规距离 |
H 桥选型上,常用集成驱动芯片如 BTS7960,两路 PWM 输入,占空比决定速度,方向由哪一路给 PWM 决定。继电器控制气泵时,要用带灭弧能力的继电器,不要把 5V 单片机引脚直接顶到继电器线圈上,中间加三极管或 ULN2803。
提示:继电器线圈和电磁阀都是感性负载,断电瞬间会产生反电动势。线圈两端并续流二极管,电磁阀靠近驱动芯片的输出端也要加,否则轻则复位,重则打坏 IO。3.3V 单片机驱动达林顿模块时,输入高电平往往不够,要把输入侧拉高到 5V 或加一级电平转换。
3.2 电机正反转与 PWM 调速的最小实现
以 BTS7960 为例,片子的 RPWM 和 LPWM 输入分别对应两个方向。正转时给 RPWM 一个占空比可调的信号、LPWM 拉低;反转时反过来。占空比范围 0 到 100。PWM 频率取 16kHz 到 20kHz,避开人耳敏感区,同时给电流采样留出稳定窗口。
// 揉捏电机控制:dir=1 正转,dir=0 反转,duty 范围 0~100 void knead_motor(unsigned char dir, unsigned char duty) { if (dir == 1) { RPWM_PIN = 1; // 方向引脚置高 LPWM_PIN = 0; pwm_set_duty(0, duty); // 通道 0 输出 PWM } else { RPWM_PIN = 0; LPWM_PIN = 1; pwm_set_duty(0, 100 - duty); } }这里pwm_set_duty是对底层 PWM 外设的封装,STC15 用 PCA 模块、STM32 用定时器通道,函数接口保持一致,换芯片时只改底层。反向时的100 - duty是为了统一“有效占空比”的语义,实际硬件的输出波形正好反过来,不用过度理解。
电机最容易被忽视的是启动电流。揉捏电机带负载静止起步时,电流能冲到额定值的好几倍,如果一上来就给 100% 占空比,过流保护必然触发。项目里普遍加软启动:前 300~500ms 把占空比从 20% 线性抬到目标值,既避免过流误动作,也让用户感觉动作更柔和。
3.3 用状态机编排揉捏、敲打、气囊动作
按摩椅的“自动模式”是把多个动作按时间顺序拼接。写这种逻辑最容易掉进 if 嵌套的坑,动作一多就变成面条代码。我会用一个枚举状态机,每个状态负责一个动作和一个持续时长,到点后先清理再切换。
typedef enum { ST_IDLE, ST_HOME, ST_KNEAD, ST_TAP, ST_AIRBAG, ST_STRETCH } MachineState; MachineState g_state = ST_IDLE; unsigned char g_sec = 0; void mode_tick(void) { switch (g_state) { case ST_KNEAD: knead_motor(1, 60); // 揉捏:正向 60% if (g_sec >= 30) { // 30 秒后切到敲打 motor_stop_all(); g_state = ST_TAP; g_sec = 0; } break; case ST_TAP: tap_motor(1, 50); // 敲打:正向 50% if (g_sec >= 20) { // 20 秒后切到气囊 motor_stop_all(); g_state = ST_AIRBAG; g_sec = 0; } break; // 其余状态类似 } }关键点是g_sec只由定时器中断里的秒计数驱动,mode_tick在 10ms 任务里周期调用,不自己延时。这样即使某个任务卡顿,模式切换的时间也不受影响。切换动作前先motor_stop_all()是为了避免两个电机同时启动造成电流叠加,这在低成本电源方案上很容易触发过流。
3.4 电流采样与电机堵转识别
按摩椅机芯被织物卡住时电机会堵转,如果程序不识别,电机会持续输出大扭矩把机芯烧毁。常见做法是低侧采样电阻加 ADC。采样电阻放在 H 桥下桥臂和 GND 之间,阻值越小损耗越低,一般 5~20mΩ 配合运放放大,单片机再读 ADC。
// 每 10ms 采集一次电机电流,连续 3 次超限判定堵转 void current_check(void) { unsigned int adc = adc_read(3); unsigned long mv = (unsigned long)adc * 3300 / 1023; unsigned int ma = (unsigned int)(mv / 5); // 采样电阻 5mΩ,放大 50 倍 if (ma > 2500) { if (++over_cnt >= 3) { motor_stop_all(); over_cnt = 0; } } else { over_cnt = 0; } }换算系数要在实际板子上用钳流表标定,代码里的 3300 和 5 只是示意。连续三次超限才动作,是为了滤掉软启动瞬间或气囊动作带来的电流尖峰。真正的堵转是持续的,三到五次采样就能稳定识别;单次超限直接停机,气囊挤压引起的波动反而会频繁误杀。
4. 按摩椅程序可靠性:主循环、看门狗与断电记忆
4.1 裸机主循环按时间片调度
按摩椅不上操作系统,但所有任务混在一个循环里写也不行。常见做法是把时间切成 1ms、10ms、100ms 三个基本节拍。1ms 级任务在定时器中断里处理,10ms 级以上放主循环。中断里只做计时和标记,具体业务全在任务函数里执行,避免中断嵌套把电机 PWM 时序打乱。
| 节拍 | 任务 | 执行位置 |
|---|---|---|
| 1ms | 红外边沿计时、PWM 占空比更新 | 定时器中断 |
| 10ms | 按键扫描、电流采样、限位检查 | 主循环 |
| 100ms | LED 刷新、加热检测 | 主循环 |
| 每轮循环 | NEC 帧处理、模式切换 | 主循环 |
主循环不能停。我一般把等待按键这类阻塞逻辑全部改成状态查询,循环里只调用各种_task()函数。这样看门狗能及时喂,遥控器按任意时刻都能被处理。要注意的是,取余判断调度在每个循环里都会执行,如果某次任务执行时间抖动,任务不会被严格均分;更稳的方式是用计数器递减,每个任务独立维护自己的下次执行时刻。
4.2 看门狗的正确喂法
看门狗的作用是发现主循环卡死并自动复位。不能在中断里喂狗,因为中断照常执行能骗过看门狗,主循环实际已经卡死。喂狗点要放在主循环几条关键调用链之后,确保一旦某个调用死循环,看门狗必然超时。
void feed_wdt(void) { WDT_CONTR = WDT_EN | WDT_CLR | WDT_PS; // STC15 使能并清除计时 } void main_loop(void) { while (1) { key_task(); // 可能阻塞的 IO 任务 motor_task(); process_nec_cmd(); feed_wdt(); // 放在最远端 } }STC 系列看门狗的溢出时间由预分频决定,具体公式要看选型手册;主频不同,同一预分频的超时时间差好几倍。工程上把喂狗周期放在看门狗超时时间的一半以内最稳妥。按摩椅这种需要频繁启停电机的设备,电机换向瞬间的干扰容易让程序跑飞,喂狗周期不要和电机控制周期绑在一起,要用独立逻辑判断。
4.3 用 IAP/EEPROM 保存档位和断电记忆
按摩椅关机再开机,用户一般希望记忆上次的模式、速度和加热状态。STC15 没有独立 EEPROM,只能把用户程序区的某个扇区划出来用 IAP 方式读写。写入前要先擦除整个扇区,擦除后该块全为 0xFF。写数据时只有“1”能变成“0”,所以不能直接修改某个字节,必须先擦后写。
| IAP 地址 | 保存内容 | 默认 |
|---|---|---|
| 0x0000 | 上次模式 | 0x00 |
| 0x0001 | 速度档 | 0x02 |
| 0x0002 | 加热开关 | 0x01 |
| 0x0010 | 出厂校准标记 | 0xAA |
void eeprom_write(unsigned int addr, unsigned char val) { IAP_CONTR = 0x80; // 使能 IAP IAP_CMD = 0x02; // 字节写 IAP_ADDRH = (addr >> 8) & 0xFF; IAP_ADDRL = addr & 0xFF; IAP_DATA = val; IAP_TRIG = 0x5A; // 触发序列,时序苛刻 IAP_TRIG = 0xA5; }IAP 操作必须配合EA=0或把中断关闭,否则写入过程中断到来会造成 IAP 时序被打断。擦写寿命按万次计算,所以项目上严格控制写次数:只有模式真正变化时才写,关机时判断当前值是否与上次写入值不同,不同才擦写,空闲时绝对不去定时刷参数。
4.4 固定串口通道与调试帧设计
按摩椅装整机后看不到内部状态,现场排查全靠预留串口。我会在固件里固定开一个 115200 的调试串口,每 100ms 输出 8 字节定长帧:帧头、状态机枚举、NEC 原始帧、电流高几位和限位状态。定长帧的好处是上位机和串口助手都能按偏移解析,不用处理粘包。
// 100ms 周期调用,发送 8 字节调试帧 void debug_report(void) { unsigned char buf[8] = {0xAA, 0x55, g_state, frame[0], frame[1], frame[2], frame[3], (unsigned char)(g_current_ma >> 4)}; uart_send(buf, 8); }生产测试时,这个调试帧可以被测试治具直接用。控制板通上电,治具通过 USB 转串口读取数据流,只要帧同步头稳定出现、帧内状态是预置的待机态,基本能确认主板电源、单片机最小系统和红外接收通道正常。整机状态下的异常,也大多能靠这 8 个字节定位到是解码层还是动作层的问题。
5. NEC 解码波形与现场按键失灵排查
5.1 用逻辑分析仪看 NEC 波形再动代码
按键没反应先别怀疑程序。逻辑分析仪单通道接红外接收头 OUT 和 GND,采样率 2MHz,触发方式选下降沿,抓 50ms。波形里有 9ms 引导码说明遥控器芯片在工作,没有就换电池或检查导电橡胶。引导码正常而数据段宽度明显不对时,才去看解码阈值。
试波时注意接收头供电电压的纹波。如果供电上叠加了电机换向产生的尖峰,解码会在某几帧随机失败,波形看起来就是忽好忽坏。给接收头电源并一个 100nF 加 47µF 电容,是按摩椅主板上最便宜的稳定性投资。
5.2 按键失灵与误触发的现场排查表
现场故障分很多种,我把最常见的几类列成了一张排查表。排查顺序永远是从接收端往动作端走,不要先拆机器看电机。
| 现象 | 优先检查 |
|---|---|
| 所有键无反应 | 接收头供电、接收头输出电平、遥控器电池 |
| 单个键无反应 | 遥控器导电橡胶、该键码值 |
| 解码有帧但动作错乱 | 映射表、LSB 位序、地址校验 |
| 偶发误触发 | 阳光/灯光干扰、重复码处理 |
动作错乱十有八九是位序问题。NEC 是 LSB 在前,如果解码时按串口收字节的 MSB 习惯写入,命令码会整体颠倒,波形却完全正常。这类问题只能在调试固件里逐个按键打印原始帧,直接和经验值对比才能暴露。
5.3 调试固件:打印原始 NEC 帧的 30 行工具
按键正确但动作错乱,最难查,因为普通万用表量不出逻辑错误。我的做法是单独写一个测试固件,只做一件事:把解码后的 4 字节原始帧从串口打出来,同时把全部电机引脚置低,避免测试时板子突然动作造成安全事故。
// 测试固件:上电后不停打印 NEC 原始帧,电机输出全部禁止 void test_main(void) { uart_init(115200); motor_pins_low(); while (1) { if (frame_ok) { uart_printf("RAW %02X %02X %02X %02X\r\n", frame[0], frame[1], frame[2], frame[3]); frame_ok = 0; } } }烧进板子后逐个按键按一遍,把串口记录下来的字节填进正式固件的映射表,按键与动作的对应关系就永远不会错。这颗测试固件在批量生产前的老化工序里同样好用:板子通电接串口,测试台自动发遥控码,比对打印结果就能判断红外接收通道。老化工序发现的问题越早越好解决,等整机装好再接串口,拆一次外壳的成本早就超过一颗芯片的价格了。
本文还有配套的精品资源,点击获取