4. 无线PWM调光
任务定义
本案例实现一个无线PWM调光系统:发送端通过按键调整PWM占空比(0%~100%,步进10%),将占空比数值以单字节形式通过NRF24L01无线发送;接收端解析数据后,利用定时器硬件PWM输出驱动LED,实现亮度调节。系统上电默认占空比为50%,每按一次按键占空比增加10%,超过100%后回绕至0%。
硬件连接
代码实现
以下代码基于STC8H系列单片机(24MHz主频),NRF24L01驱动采用SPI接口。发送端与接收端共用同一份NRF24L01底层驱动,通过宏定义区分收发角色。PWM使用定时器0产生20kHz频率,占空比由捕获比较寄存器控制。
// ============ 公共头文件 nrf24l01.h ============
#ifndef NRF24L01_H
#define NRF24L01_H
#include <STC8H.H>
// SPI引脚定义
#define CE P1_0
#define CSN P1_1
#define SCK P1_2
#define MOSI P1_3
#define MISO P1_4
// NRF24L01寄存器命令
#define R_REGISTER 0x00
#define W_REGISTER 0x20
#define R_RX_PAYLOAD 0x61
#define W_TX_PAYLOAD 0xA0
#define FLUSH_TX 0xE1
#define FLUSH_RX 0xE2
// 寄存器地址
#define CONFIG 0x00
#define EN_AA 0x01
#define EN_RXADDR 0x02
#define SETUP_AW 0x03
#define SETUP_RETR 0x04
#define RF_CH 0x05
#define RF_SETUP 0x06
#define STATUS 0x07
#define TX_ADDR 0x10
#define RX_ADDR_P0 0x0A
// 状态位
#define TX_DS (1<<5)
#define RX_DR (1<<6)
void SPI_Init(void);
void NRF24_Init(void);
void NRF24_WriteReg(unsigned char reg, unsigned char val);
unsigned char NRF24_ReadReg(unsigned char reg);
void NRF24_SendByte(unsigned char data);
unsigned char NRF24_ReceiveByte(void);
void NRF24_TX_Mode(void);
void NRF24_RX_Mode(void);
unsigned char NRF24_Send(unsigned char data);
unsigned char NRF24_Receive(unsigned char *data);
#endif
// ============ NRF24L01底层驱动 nrf24l01.c ============
#include "nrf24l01.h"
void SPI_Init(void) {
P1M1 = 0x10; // MISO设为输入
P1M0 = 0x0F; // CE,CSN,SCK,MOSI设为推挽输出
SPI_InitTypeDef spi = {0};
spi.SPI_Mode = SPI_Mode_Master;
spi.SPI_Speed = SPI_Speed_4; // 6MHz
spi.SPI_CPOL = SPI_CPOL_Low;
spi.SPI_CPHA = SPI_CPHA_1Edge;
SPI_Init(&spi);
SPI_Cmd(ENABLE);
}
void NRF24_WriteReg(unsigned char reg, unsigned char val) {
CSN = 0;
SPI_WriteByte(W_REGISTER | reg);
SPI_WriteByte(val);
CSN = 1;
}
unsigned char NRF24_ReadReg(unsigned char reg) {
unsigned char val;
CSN = 0;
SPI_WriteByte(R_REGISTER | reg);
val = SPI_ReadByte();
CSN = 1;
return val;
}
void NRF24_TX_Mode(void) {
CE = 0;
NRF24_WriteReg(CONFIG, 0x0E); // 上电, TX模式
NRF24_WriteReg(EN_AA, 0x01); // 使能自动ACK
NRF24_WriteReg(EN_RXADDR, 0x01);
NRF24_WriteReg(SETUP_AW, 0x03); // 5字节地址
NRF24_WriteReg(SETUP_RETR, 0x1A); // 自动重发
NRF24_WriteReg(RF_CH, 40);
NRF24_WriteReg(RF_SETUP, 0x26); // 2Mbps, 0dBm
// 设置TX地址和RX地址(用于ACK)
unsigned char addr[5] = {0xE7,0xE7,0xE7,0xE7,0xE7};
CSN = 0;
SPI_WriteByte(W_REGISTER | TX_ADDR);
for(int i=0;i<5;i++) SPI_WriteByte(addr[i]);
CSN = 1;
CSN = 0;
SPI_WriteByte(W_REGISTER | RX_ADDR_P0);
for(int i=0;i<5;i++) SPI_WriteByte(addr[i]);
CSN = 1;
CE = 1;
}
void NRF24_RX_Mode(void) {
CE = 0;
NRF24_WriteReg(CONFIG, 0x0F); // 上电, RX模式
NRF24_WriteReg(EN_AA, 0x01);
NRF24_WriteReg(EN_RXADDR, 0x01);
NRF24_WriteReg(SETUP_AW, 0x03);
NRF24_WriteReg(RF_CH, 40);
NRF24_WriteReg(RF_SETUP, 0x26);
unsigned char addr[5] = {0xE7,0xE7,0xE7,0xE7,0xE7};
CSN = 0;
SPI_WriteByte(W_REGISTER | RX_ADDR_P0);
for(int i=0;i<5;i++) SPI_WriteByte(addr[i]);
CSN = 1;
CE = 1;
}
unsigned char NRF24_Send(unsigned char data) {
CE = 0;
CSN = 0;
SPI_WriteByte(FLUSH_TX);
CSN = 1;
CSN = 0;
SPI_WriteByte(W_TX_PAYLOAD);
SPI_WriteByte(data);
CSN = 1;
CE = 1; // 启动发送
// 等待发送完成或超时(10ms)
unsigned int timeout = 10000;
while(timeout--) {
unsigned char status = NRF24_ReadReg(STATUS);
if(status & TX_DS) {
NRF24_WriteReg(STATUS, TX_DS); // 清除中断
return 1; // 发送成功
}
}
return 0; // 超时失败
}
unsigned char NRF24_Receive(unsigned char *data) {
unsigned char status = NRF24_ReadReg(STATUS);
if(status & RX_DR) {
CSN = 0;
SPI_WriteByte(R_RX_PAYLOAD);
*data = SPI_ReadByte();
CSN = 1;
NRF24_WriteReg(STATUS, RX_DR);
return 1;
}
return 0;
}
// ============ 发送端 main_tx.c ============
#include "nrf24l01.h"
// 按键引脚 P3.2
sbit KEY = P3^2;
// PWM输出(调试用, 可接LED观察本地状态)
sbit PWM_OUT = P2^0;
void Timer0_Init(void) {
// 20kHz PWM, 定时器0模式0(16位)
AUXR |= 0x80; // 1T模式
TMOD &= 0xF0;
TMOD |= 0x01;
TH0 = 0xFF;
TL0 = 0x06; // 20kHz重载值
ET0 = 1;
TR0 = 1;
}
unsigned char duty = 50; // 默认50%
unsigned int pwm_count = 0;
void Timer0_ISR(void) interrupt 1 {
TH0 = 0xFF;
TL0 = 0x06;
pwm_count++;
if(pwm_count >= 100) pwm_count = 0;
PWM_OUT = (pwm_count < duty) ? 1 : 0;
}
void main(void) {
SPI_Init();
NRF24_TX_Mode();
Timer0_Init();
EA = 1;
while(1) {
if(KEY == 0) { // 按键按下
unsigned int delay = 5000;
while(delay--); // 消抖
if(KEY == 0) {
duty += 10;
if(duty > 100) duty = 0;
// 等待按键释放
while(KEY == 0);
// 发送占空比
NRF24_Send(duty);
}
}
}
}
// ============ 接收端 main_rx.c ============
#include "nrf24l01.h"
// PWM输出引脚 P2.0
sbit PWM_OUT = P2^0;
void Timer0_Init(void) {
AUXR |= 0x80;
TMOD &= 0xF0;
TMOD |= 0x01;
TH0 = 0xFF;
TL0 = 0x06; // 20kHz
ET0 = 1;
TR0 = 1;
}
unsigned char duty = 50; // 默认50%
unsigned int pwm_count = 0;
void Timer0_ISR(void) interrupt 1 {
TH0 = 0xFF;
TL0 = 0x06;
pwm_count++;
if(pwm_count >= 100) pwm_count = 0;
PWM_OUT = (pwm_count < duty) ? 1 : 0;
}
void main(void) {
SPI_Init();
NRF24_RX_Mode();
Timer0_Init();
EA = 1;
unsigned char rx_data;
while(1) {
if(NRF24_Receive(&rx_data)) {
if(rx_data <= 100) { // 合法性检查
duty = rx_data; // 更新占空比
}
}
}
}
编译与烧录
使用Keil C251编译器,创建两个工程分别编译发送端和接收端代码。发送端工程包含main_tx.c和nrf24l01.c,接收端工程包含main_rx.c和nrf24l01.c。编译生成HEX文件后,通过STC-ISP软件分别烧录到两块STC8H开发板。
验证方法
按以下步骤验证系统功能:
- 发送端和接收端各接一个LED(发送端LED接P2.0用于本地观察,接收端LED接P2.0作为调光目标);
- 两块板均上电,观察接收端LED应保持50%亮度(默认占空比);
- 按下发送端按键一次,发送端本地LED亮度应变为60%亮度(占空比增加10%),同时接收端LED亮度同步变为60%;
- 连续按按键10次,占空比从50%→60%→…→100%→0%→10%,观察两端LED亮度同步变化;
- 用示波器测量接收端P2.0引脚波形,确认频率约为20kHz,占空比与按键次数对应。
判定标准:每次按键后,接收端LED亮度与发送端本地LED亮度一致,且示波器测得的占空比数值与预期值(50%、60%、…、100%、0%、10%…)完全吻合,即判定为通过。