早以前,Wifi还不是很普及的时候,我为了组建一个小型网络,使用了NRF24L01无线收发模块,同时为了减小整个电路板的体积,使用了89C2051单片机。当时还不知道有MDK这种开发工具。整个程序都是用51汇编语言写的。当时为了测试收发处理,是在面包板上使用相同的电路搭建的。
AT89C2051单片机芯片引脚图:
NRF24L01模块:
NRF24L01的接线:
NRF24L01使用的标准SPI通讯接口,因此处理起来比较简单。
AT89C2051单片机本身是支持低电压的,所以二者在一起使用,不用在工作电压上做过多处理,统一使用3.3V就好。二者的连接电路图如下:
电路图很简单,就是为了测试能否控制NRF24L01相互间通讯。一侧按下按键的时候,启动发送。另一组接收到数据后,点亮对应的发光管。AT89C2051的P1.0和P1.1作为IO口使用的时候,因为是开漏的,需要上拉电阻。在本电路中直接作为驱动LED的,因为由限流电阻和LED构成上拉电路。
在接收处理中,没有使用AT89C2051本身的外部中断处理NRF24L01产生的中断,而是使用轮巡P1.7上的电平变化来判断是否发生中断。
程序用的是汇编语言,现在估计没多少人还在用51单片机以及汇编语言了。但作为初学者,学习汇编,在理解单片机的工作过程上还是会有很大帮助的。同时由于AT89C2051的内存和Flash空间比较小,使用汇编的话,在有效利用空间上,还是有好处的。以下是程序,因为有注释,应该会容易理解的:
;为了防止在从机应答没有完成过程中,又再次发射数据,导致接收端异常,需要加必要的处理 ;比如时间之类定时中断,防止IRQ管脚处于高电平,导致程序进入死循环 ;主要是判断IRQ==0的处理段 ;用于设备控制的时候,通讯最好采用两个字节,防止但字节时通讯错误,导致设备误动作 ;比如c0~c9,ca o0~o9,oa之类的双字节ASCII码 c:close, o:open, a:all,0-9:设备编号 LED0 BIT P1.0 LED1 BIT P1.1 CE BIT P1.2 CSN BIT P1.3 SCK BIT P1.4 MOSI BIT P1.5 MISO BIT P1.6 IRQ BIT P1.7 KEY34 BIT P3.4 KEY35 BIT P3.5 ;发射地址长度:5字节 TX_ADDR_WIDTH EQU 5 ;按钮是否按下的标志.按下:1; UNIT_LOCK_KEY EQU 03H ;R3 ;发射地址内存保持区 UNIT_TXADDR EQU 08H ;TX_ADDR, 5Byte(08H~0DH) ;发射缓冲区/发射缓冲区宽度,即发送接收数据的长度 TR_DATA_WIDTH EQU 1 ;发射缓冲区,1字节(控制16个设备):形如10~1F:打开0号~15号设备,00~0F:关闭0号~15号设备 UNIT_TX_BUF EQU 0EH ; ;接收缓冲区,1字节(控制16个设备):形如O0~OF:打开0号~15号设备,C0~CF:关闭0号~15号设备 UNIT_RX_BUF EQU 0FH ; ;临时变量 UNIT_20H EQU 20H ;st : st1-8 UNIT_21H EQU 21H ;st1: st11-18 UNIT_22H EQU 22H ;sta: (22H.4: MAX_RT, 22H.5: TX_DS, 22H.6:RX_DR) ORG 0000H Q0000: LJMP StartUp ;======================================0003 ORG 0003H StartUp: MOV SP, #40H ;设置堆栈指针 MOV P1, #255 MOV P3, #255 ;发射地址调入内存?直接用DPTR读是否一样? MOV R7, #TX_ADDR_WIDTH MOV R0, #UNIT_TXADDR MOV DPTR, #DAT_TX_ADDR suSetAddr: CLR A MOVC A, @A+DPTR MOV @R0, A INC R0 INC DPTR DJNZ R7, suSetAddr MAIN: CLR A MOV 20H,A ;st MOV 21H,A ;st1 MOV 22H,A ;sta MOV UNIT_LOCK_KEY, #0 ;//lock_key LCALL Delay100ms LCALL InitNfr2410 LCALL Delay100ms LCALL Delay100ms LCALL SetRxMode ;检查是否有按键按下(P3.4 or P3.5) CheckKeyPress: LCALL EmptyOpt SETB KEY34 SETB KEY35 CheckKeyPress1: JNB KEY34,KeyPressed JB KEY35,Q00A8 KeyPressed: SETB IRQ ;写状态寄存器 MOV R5, #0FFH ;写数据255 MOV R7, #27H ;写状态寄存器 LCALL SpiRwReg ;执行写操作 ;判断是哪个按键按下 JB KEY34, KeyPressed1 ;P3.4按键被按下的时候 MOV R0, #UNIT_TX_BUF MOV @R0, #0AAH LJMP KeyPressed2 KeyPressed1: JB KEY35, KeyPressed2 ;P3.5按键被按下的时候 MOV R0, #UNIT_TX_BUF MOV @R0, #55H ;判断按键按下处理完毕 KeyPressed2: LCALL EmptyOpt ;设置NRF24L01进入发射模式 LCALL SetTxMode WaitIRQClr1: JNB IRQ, CheckIRQType LCALL EmptyOpt LJMP WaitIRQClr1 ;读取NRF24L01状态值,判断中断的类型 CheckIRQType: MOV R7, #07H LCALL SpiRead ;状态值暂时保存在22H中 MOV 22H, R7 ;清除NRF24L01的所有中断指示 MOV R5, #0FFH MOV R7, #27H LCALL SpiRwReg ;取出状态值,判断是什么中断 MOV A, 22H ;是没有收到正常发射完成的中断,转L_SendNg JNB ACC.5, L_SendNg ;建立指示灯交替亮灭,表示发射正常 CPL LED0 ;判断是哪个按钮被按下的,此处代码没有意义,仅用于显示 JB KEY34, Q0087 ;是P3.4对应的按钮 LCALL Delay100ms LCALL Delay100ms LJMP Q0090 ;是P3.5对应的按钮 Q0087: LCALL Delay100ms LCALL Delay100ms Q0090: LCALL EmptyOpt LJMP Q00A0 ;清除NRF24L01收发缓冲区 L_SendNg: LCALL Nrf2401ClearAll LCALL EmptyOpt Q00A0: ;建立按钮被按下的标志 MOV UNIT_LOCK_KEY, #01H LJMP CheckKeyPress1 ;检查是否有按钮被按下标志 Q00A8: MOV A, UNIT_LOCK_KEY ;没有按钮按下,转Q00C7 JZ Q00C7 ;有按钮被按下过 ;清除标志 MOV UNIT_LOCK_KEY, #0 ;使NRF24L01处于读状态 LCALL SetRxMode ;拉高中断管脚电平 SETB IRQ ;等待在此产生中断(收到从机的应答中断) Q00B8: JB IRQ, Q00C0 LCALL EmptyOpt LJMP Q00B8 ;没有收到从机的应答,延时 Q00C0: MOV R7, #90H MOV R6, #01H LCALL DelayUs ;此处是用来响应从机主动发射数据场合 Q00C7: SETB IRQ ;若发生中断,转L_RecvOK,处理接收 JNB IRQ, L_RecvOK ;否则继续转监视按键处理 LJMP CheckKeyPress L_RecvOK: LCALL EmptyOpt ;取得NRF24L01状态 MOV R7, #07H LCALL SpiRead ;缓存到22H中 MOV 22H, R7 ;清除中断状态指示 MOV R5, #0FFH MOV R7, #27H LCALL SpiRwReg ;判断中断类型 MOV A, 22H ;如果没有接收到从机数据的中断,转L_RecvNg JNB ACC.6, L_RecvNg ;接收到从机数据的中断 LCALL EmptyOpt ;交替亮灭指示灯 CPL LED1 ;取得主机发送过来的数据,保存到内存中的缓冲区中 MOV R1, #UNIT_RX_BUF MOV R2, #TR_DATA_WIDTH MOV R7, #61H LCALL SpiReadBuf ;确认得到的标志数据是不是合法的值范围 ;从缓冲区取出数据,并判断值 MOV R0, #UNIT_RX_BUF MOV A, @R0 ;判断是哪个按钮按下对应的发送值 CJNE A, #0AAH, Q0112 ;等于AAH时 LCALL Delay100ms LCALL Delay100ms LJMP L_ClearRecvBuffer ;不是AAH, 是55H吗? Q0112: MOV R0, #UNIT_RX_BUF MOV A, @R0 ;是55H吗? 不是,则转L_ClearRecvBuffer CJNE A, #55H, L_ClearRecvBuffer LCALL Delay100ms LCALL Delay100ms ;接收到的,不是按钮按下的标志数据(AAH, 55H) L_ClearRecvBuffer: CLR A MOV R0, #UNIT_RX_BUF MOV @R0, A LCALL EmptyOpt ;继续转监视按键处理 LJMP CheckKeyPress ;没有收到接收中断 L_RecvNg: NOP LCALL EmptyOpt ;清除NRF24L01缓冲数据 LCALL Nrf2401ClearAll ;设置NF24L01进入接收模式 LCALL SetRxMode SETB IRQ Q013E: JB IRQ, Q0146 LCALL EmptyOpt LJMP Q013E Q0146: NOP LJMP CheckKeyPress ;========================================01D7 SpiRw: ;从R7取得读写数据 MOV 20H,R7 ;输出字节的B7位 MOV C,20H.7 ;07H MOV MOSI,C ;输出到管脚上 SETB SCK ;发出脉冲高电平 MOV C,MISO ;从MISO管脚取得输入 MOV 21H.7,C ;保存到21H中(为了不使用21H,此处用B,P2代替如何) MOV B.7, C CLR SCK ;发出脉冲低电平,完成一位的输出操作 ;输出字节的B6位 MOV C,20H.6 MOV MOSI,C SETB SCK MOV C,MISO MOV 21H.6,C CLR SCK ;输出字节的B5位 MOV C,20H.5 MOV MOSI,C SETB SCK MOV C,MISO MOV 21H.5,C CLR SCK ;输出字节的B4位 MOV C,20H.4 MOV MOSI,C SETB SCK MOV C,MISO MOV 21H.4,C CLR SCK ;输出字节的B3位 MOV C,20H.3 MOV MOSI,C SETB SCK MOV C,MISO MOV 21H.3,C CLR SCK ;输出字节的B2位 MOV C,20H.2 MOV MOSI,C SETB SCK MOV C,MISO MOV 21H.2,C CLR SCK ;输出字节的B1位 MOV C,20H.1 MOV MOSI,C SETB SCK MOV C,MISO MOV 21H.1,C CLR SCK ;输出字节的B0位 MOV C,20H.0 MOV MOSI,C SETB SCK MOV C,MISO MOV 21H.0,C CLR SCK ;取得的返回值回存到R7中 MOV R7,21H ; PUSH ACC ; MOV A, R7 ; MOV R6, #8 ;rwloop: MOV MOSI, ACC.7 ; RL A ; ANL A, #0FEH ; ORL A, MISO ; MOV R7, A ; DJNZ R6, rwloop ; POP ACC RET ;=====================================023C SetTxMode: ;进入待机模式 LCALL PowerOff CLR CE ;写发送地址 MOV R1, #UNIT_TXADDR ;发射端物理地址的首地址 MOV R2, #TX_ADDR_WIDTH ;地址宽度 MOV R7, #30H ;写发送地址寄存器操作:20H+10H(发送地址寄存器) LCALL SpiWriteBuf ;写数据通道0地址 MOV R1, #UNIT_TXADDR ;发射端物理地址的首地址 MOV R2, #TX_ADDR_WIDTH ;地址宽度 MOV R7, #2AH ;写数据通道0地址操作 20H+0AH(数据通道0地址寄存器) LCALL SpiWriteBuf ;把要发射的数据写入NRF24L01的发送缓冲寄存器中 MOV R1, #UNIT_TX_BUF ;发射数据缓冲区首地址 MOV R2, #TR_DATA_WIDTH ;发射的数据长度 MOV R7, #0A0H ;写入NRF24L01的发送缓冲寄存器操作:A0 LCALL SpiWriteBuf ;允许自动应答(只允许数据通道0) MOV R5, #01H ;只允许数据通道0 MOV R7, #21H ;写自动应答寄存器 LCALL SpiRwReg ;设置允许接收的数据通道(只允许数据通道0) MOV R5, #01H ;只允许数据通道0 MOV R7, #22H ;写接收地址允许寄存器 LCALL SpiRwReg ;设置自动重发参数 MOV R5, #1AH ;重发延时500+86us, 重发10次 MOV R7, #24H ;写自动重发寄存器操作 LCALL SpiRwReg ;设置射频通道的工作频率 MOV R5, #28H ; MOV R7, #25H ;写射频通道设置寄存器操作 LCALL SpiRwReg ;设置射频参数 MOV R5, #26H ;0db, 1Mbps,低噪声增益放大 MOV R7, #26H ;写射频参数寄存器操作(6号) LCALL SpiRwReg ;启动发射 MOV R5, #0EH ;允许中断时IRQ脚电平拉低.允许CRC,16位CRC,发射模块上电,发射 MOV R7, #20H ;写配置寄存器操作(0号) LCALL SpiRwReg SETB CE RET ;============================================ ;SetRxMode-0298 SetRxMode: LCALL PowerOff CLR CE ;写数据通道0地址 MOV R1, #UNIT_TXADDR ;发射端物理地址的首地址 MOV R2, #TX_ADDR_WIDTH ;地址宽度 MOV R7, #2AH ;写数据通道0地址操作 20H+0AH(数据通道0地址寄存器) LCALL SpiWriteBuf ;允许自动应答(只允许数据通道0) MOV R5, #01H ;只允许数据通道0 MOV R7, #21H ;写自动应答寄存器 LCALL SpiRwReg ;设置允许接收的数据通道(只允许数据通道0) MOV R5, #01H ;只允许数据通道0 MOV R7, #22H ;写接收地址允许寄存器 LCALL SpiRwReg ;设置射频通道的工作频率 MOV R5, #28H ; MOV R7, #25H ;写射频通道设置寄存器操作(5) LCALL SpiRwReg ;设置接收通道0的数据宽度 MOV R5, #TR_DATA_WIDTH ;1个字节 MOV R7, #31H ;写数据通道0的数据宽度寄存器操作(11H) LCALL SpiRwReg ;设置射频参数 MOV R5, #26H ;0db, 1Mbps,低噪声增益放大 MOV R7, #26H ;写射频参数寄存器操作(6号) LCALL SpiRwReg ;启动接收 MOV R5, #0FH ;允许中断时IRQ脚电平拉低.允许CRC,16位CRC,发射模块上电,接收 MOV R7, #20H ;写配置寄存器操作(0号) LCALL SpiRwReg SETB CE RET ;========================================== ;R2:读写NRF24L01数据缓冲区的字节数,用于参数传递 ;R1:保持发送数据的内存地址 ;R7:操作指令(含目标寄存器地址) SpiWriteBuf: ;允许SPI操作 CLR CSN ;写入R7指令和寄存器地址 LCALL SpiRw ;保存返回值 MOV 06H, R7 ;循环处理完指定的字节数,字节数保存在R2中 ;取出缓冲区的一个字节数据 swbloop: MOV A, @R1 ;存入R7 MOV R7, A ;发给NRF24L01 LCALL SpiRw ;指向下一个缓冲区单元 INC R1 ;处理完所有字节了吗? DJNZ R2, swbloop SETB CSN MOV R7, 06H RET ;======================================= ;R2:读写NRF24L01数据缓冲区的字节数,用于参数传递 ;R1:保持接收数据的内存地址 ;R7:操作指令(含目标寄存器地址) SpiReadBuf: ;允许SPI操作 CLR CSN ;写入R7指令和寄存器地址 LCALL SpiRw ;保存返回值 MOV 06H, R7 ;循环处理完指定的字节数,字节数保存在R2中 ;取出NRF24L01接收缓冲区的一个字节数据 srbloop: MOV R7, #0 LCALL SpiRw MOV A, R7 MOV @R1, A ;指向下一个缓冲区单元 INC R1 ;处理完所有字节了吗? DJNZ R2, srbloop SETB CSN MOV R7, 06H RET ;======================================= ;R7中的值用来区分清空对象:发送FIFO 还是接收FIFO SpiClrReg: CLR CSN CJNE R7,#01H,scr1 ;清空发送FIFO缓冲区 MOV R7,#0E1H LCALL SpiRw LJMP scr2 ;清空接收FIFO缓冲区 scr1: MOV R7,#0E2H LCALL SpiRw scr2: SETB CSN RET ;======================================= Nrf2401ClearAll: CLR A MOV R7,A LCALL SpiClrReg MOV R7,#01H LCALL SpiClrReg MOV R5,#0FFH MOV R7,#27H LCALL SpiRwReg SETB IRQ RET ;======================================= DelayUs: CLR A MOV R5,A dus1: MOV A,R7 ORL A,R6 JZ dus4 CLR A MOV R5,A dus2: INC R5 CJNE R5,#01H,dus2 MOV A,R7 DEC R7 JNZ dus3 DEC R6 dus3: LJMP dus1 dus4: RET ;======================================= SpiRwReg: MOV 06H, 05H CLR CSN LCALL SpiRw MOV 05H, R7 MOV R7, 06H LCALL SpiRw SETB CSN MOV R7, 05H RET ;======================================= PowerOff: CLR CE MOV R5,#0DH MOV R7,#20H LCALL SpiRwReg SETB CE MOV R7,#14H MOV R6,#00H LCALL DelayUs RET ;======================================= Delay100ms: NOP NOP MOV R7,#09H MOV R6,#68H MOV R5,#8BH dms1: DJNZ R5,$ DJNZ R6,dms1 DJNZ R7,dms1 RET ;======================================== SpiRead: CLR CSN LCALL SpiRw CLR A MOV R7,A LCALL SpiRw SETB CSN RET ;======================================== InitNfr2410: CLR CE SETB CSN CLR SCK RET ;======================================== ;可以删除,没有实际处理 EmptyOpt: NOP NOP RET ;======================================== ;物理地址 DAT_TX_ADDR: DB 34H, 43H, 10H, 10H, 01H, 01H ;字形笔段对应区 DAT_SM_MAP: DB 0C0H, 0F9H,0A4H,0B0H, 99H, 92H, 82H,0F8H, 80H, 90H,0BFH,0FFH END
NRF24L01在收到有效数据后,就会发出中断,因此在实际应用中,特别在多对多环境时,有必要建立自己的通讯协议,在通讯数据上加上地址编号之类的数据,在接收时检查地址与自己所在的编号是否一致,来确定数据是要发给谁。我这里只是测试两个模块之间的收发,因此没有加通讯协议。