本帖最后由 xld0932 于 2023-7-19 13:24 编辑
@安小芯 @21小跑堂
概述 基于N32G430C8L7-STB开发板搭配外围功能模块实现可视呼叫门铃的功能演示。通过板载的按键检测实现门铃呼叫、开锁功能;当按键呼叫按键后,通过摄像头将门口景象以静态图片的形式显示在TFT显示屏上,同时播放门铃音乐;当按键开锁按键后,在TFT显示屏上显示开锁提示。功能描述虽然简单,但使用的MCU外设技术点不少哦,涉及到了GPIO、SPI、I2S、DMA、UART、FLASH、RTC等,主要是想通过实现这个演示样机的功能来熟悉国民MCU产品,最后为了完成上述功能还需要在标准板的基础上再画一块实现上述功能的扩展板。
软件功能 - 通过板载的按键实现模拟门铃呼叫按键、开锁的功能;
- 通过USART2与RS485模块连接,RS485模块连接到远端的门口摄像头,通过发送指令的形式抓拍图片,再通过指令回传图片数据;
- 通过SPI接口连接SPI接口的TFT显示屏,显示相关信息(**、开锁提示、摄像头图片、音频文件等);
- 通过SPI接口连接SPI FLASH存储模块,在存储模块中存放GBK中文字库、**图片数据等信息;通过PC端的SecureCRT软件与N32G430C8L7-STB板载的USART1进行通讯,将资源文件通过Xmodem协议下载到SPI FLASH存储模块中;
- 通过I2S接口连接CS4344音频模块,通过音频线将CS4344模块与音箱相连接;
系统框图 由于N32G430C8L7芯片资源只带有64KB的FLASH和16KB的SRAM空间,所以需要扩展SPI FLASH来存放一些资源文件;在系统中使用I2S来播放WAV文件,为了达到播放音频流畅的效果,WAV文件的读取使用了双缓存结合DMA的实现方式,所以占用了部分的SRAM空间;对于RS485摄像头回传的图片数据是JPEG格式的,所以在实现方案中我们使用了TJPGD3软件解码库,将JPEG格式的图片数据进行解析,并显示到TFT显示屏上,对于软件解码JPEG图片需要占用不少的SRAM的空间,这样留给摄像头图片读回存放的空间就已经不够用了,所以为了解决这个问题,我们想到将摄像头读回的JPEG图片数据暂存在MCU内部FLASH空间,这样就解决了SRAM空间不足的问题了!
硬件准备 - N32G430C8L7-STB开发板
- N32G430C8L7-STB_EBK功能扩展板(RS485模块、CS4344模块、SPI FLASH模块、TFT显示屏)
- RS485接口摄像头
- 音箱
- 相关数据连接线
N32G430C8L7-STB_EBK功能扩展板 原理图
PCB-2D正面图
焊接成品
资源文件 GBK中文字库存放在SPI FLASH起始地址为0的位置、音频文件存放在SPI FLASH起始地址为0x100000的位置、**等图片信息存放在SPI FLASH起始地址为0x200000的位置;在下载资源到SPI FLASH时,需要将config.h文件中宏定义修改为如下所示:
然后打开PC端的SecureCRT软件下载资源文件,输入的命令为XMODEM 1 XXXXX,其中1表示PC端将数据下传到MCU,经MCU将数据写入到SPI FLASH,第二个参数是指令下载的起始地址;XMODEM协议在传输时带有校验和重传机制,所以在出现ERROR后不用担心数据丢失或者数据不正确,如下图所示:
在TFT初始显示界面上有两个**显示,如果这些图片存放在MCU FLASH空间会导致后面的功能代码空间不足,所以就存放在SPI FLASH存储了;在取模时需要结合代码的实现来设定,如下所示:
其中勾选了包含图像头数据为了获取图片的宽度、高度信息,方便程序中排版显示。
代码实现 代码部分仅摘录了部分,完整的功能代码可以下载文末的软件工程源代码附件。
摄像头相关部分 void CAMERA_RxHandler(void)
{
static uint8_t RxStart = 0;
static uint16_t RxIndex = 0;
static uint8_t RxBuffer[600];
static uint32_t RxLength = 0;
if(QUEUE_EMPTY(QUEUE_CAMERA_IDX) == 0)
{
uint8_t RxData = QUEUE_READ(QUEUE_CAMERA_IDX);
if(RxStart == 0)
{
if(RxData == CAMERA_START)
{
RxStart = 1;
RxIndex = 0;
memset(RxBuffer, 0, sizeof(RxBuffer));
RxBuffer[RxIndex++] = RxData;
}
}
else
{
if(RxIndex == 1)
{
RxBuffer[RxIndex++] = RxData;
}
else
{
RxBuffer[RxIndex++] = RxData;
switch(RxBuffer[1])
{
case CAMERA_CMD_H:
if((RxIndex == 4) && (RxBuffer[RxIndex-1] == CAMERA_END))
{
RxStart = 0;
}
break;
case CAMERA_CMD_R:
if((RxIndex == 10) && (RxBuffer[RxIndex-1] == CAMERA_END))
{
RxStart = 0;
CAMERA_PictureSize = RxBuffer[6];
CAMERA_PictureSize <<= 8;
CAMERA_PictureSize |= RxBuffer[5];
CAMERA_PictureSize <<= 8;
CAMERA_PictureSize |= RxBuffer[4];
CAMERA_PictureSize <<= 8;
CAMERA_PictureSize |= RxBuffer[3];
CAMERA_PacketTotal = RxBuffer[8];
CAMERA_PacketTotal <<= 8;
CAMERA_PacketTotal |= RxBuffer[7];
CAMERA_CMD_Running = 0;
}
break;
case CAMERA_CMD_E:
if((RxIndex == 4) && (RxBuffer[RxIndex-1] == CAMERA_END))
{
RxStart = 0;
}
break;
case CAMERA_CMD_F:
if(CAMERA_PacketIndex < CAMERA_PacketTotal)
{
RxLength = CAMERA_PACKET_SIZE;
}
else
{
RxLength = CAMERA_PictureSize % CAMERA_PACKET_SIZE;
}
if(RxIndex == (RxLength + 9))
{
RxStart = 0;
uint16_t PacketIndex = 0;
uint16_t PictureSize = 0;
uint32_t SRAM_Offset = 0;
PacketIndex = RxBuffer[4];
PacketIndex <<= 8;
PacketIndex |= RxBuffer[3];
PictureSize = RxBuffer[6];
PictureSize <<= 8;
PictureSize |= RxBuffer[5];
SRAM_Offset = (PacketIndex - 1) * CAMERA_PACKET_SIZE;
if(((PacketIndex - 1) % 4) == 0)
{
FLASH_Unlock();
FLASH_One_Page_Erase(0x0800A000 + SRAM_Offset);
FLASH_Lock();
}
FLASH_Unlock();
for(uint16_t i = 0; i < PictureSize; i+=4)
{
FLASH_Word_Program(i+SRAM_Offset+0x0800A000, *((uint32_t *)(&RxBuffer[i+7])));
}
FLASH_Lock();
CAMERA_CMD_Running = 0;
}
break;
case CAMERA_CMD_D: break;
case CAMERA_CMD_I: break;
case CAMERA_CMD_Q: break;
default:
break;
}
}
}
}
}
TFT相关部分 void LCD_ShowEN(uint16_t X, uint16_t Y, char ch)
{
uint8_t Data = 0;
uint16_t Color = 0;
for(uint8_t i = 0; i < 16; i++)
{
Data = ASCII_1608[ch - 32][i];
for(uint8_t j = 0; j < 8; j++)
{
if((Data >> j) & 0x01) Color = LCD_Forecolor;
else Color = BACKCOLOR;
LCD_DrawPoint(X+j, Y+i, Color);
}
}
}
void LCD_ShowCN(uint16_t StartX, uint16_t StartY, const char *str)
{
uint8_t Buffer[32];
uint8_t Array[16][16];
uint8_t Point[16][16];
memset(Buffer, 0xFF, sizeof(Buffer));
SPI_FLASH_GetGBK(str, Buffer);
for(uint8_t i = 0; i < 8; i++)
{
for(uint8_t j = 0; j < 4; j++)
{
uint8_t Data = Buffer[i*4+j];
for(uint8_t k = 0; k < 4; k++)
{
if(Data & (0x08 >> (k-0))) Array[i*2+0][j*4+k-0] = 1;
else Array[i*2+0][j*4+k-0] = 0;
}
for(uint8_t k = 4; k < 8; k++)
{
if(Data & (0x80 >> (k-4))) Array[i*2+1][j*4+k-4] = 1;
else Array[i*2+1][j*4+k-4] = 0;
}
}
}
for(uint8_t i = 0; i < 16; i++)
{
for(uint8_t j = 0; j < 16; j++)
{
Point[i][j] = Array[j][15-i];
}
}
for(uint8_t i = 0; i < 16; i++)
{
for(uint8_t j = 0; j < 16; j++)
{
if(Point[15-i][j])
{
LCD_DrawPoint(StartX+i, StartY+j, LCD_Forecolor);
}
else
{
LCD_DrawPoint(StartX+i, StartY+j, BACKCOLOR);
}
}
}
}
void LCD_ShowLOG(uint16_t StartX, uint16_t StartY, const char *str)
{
while(*str != '\0')
{
if(*str < 0x7F)
{
if(StartX > (LCD_WIDTH - 8))
{
StartX = 0; StartY += 16;
}
if(StartY > (LCD_HEIGHT - 16))
{
StartX = 0; StartY = 0;
LCD_ClearScreen(BACKCOLOR);
}
LCD_ShowEN(StartX, StartY, *str);
StartX += 0x08;
str += 0x01;
}
else
{
if(StartX > (LCD_WIDTH - 16))
{
StartX = 0; StartY += 16;
}
if(StartY > (LCD_HEIGHT - 16))
{
StartX = 0; StartY = 0;
LCD_ClearScreen(BACKCOLOR);
}
LCD_ShowCN(StartX, StartY, str);
StartX += 0x10;
str += 0x02;
}
}
}
void LCD_DrawImage(void)
{
uint16_t StartX = 0, StartY = 0;
uint16_t Width = 0, Height = 0, Index = 0;
uint32_t Address = 0, Offset = 0, Length = 0;
uint8_t Buffer[200];
Address = 0x20204C;
Offset = 0;
Length = 0x001A84;
Index = 8;
SPI_FLASH_FastRead(Address + Offset, Buffer, 8);
Width = Buffer[2];
Width <<= 8;
Width |= Buffer[3];
Height = Buffer[4];
Height <<= 8;
Height |= Buffer[5];
StartX = (240 - Width ) / 2;
StartY = 30;
LCD_SetAddress(StartX, StartY, StartX + Width - 1, StartY + Height - 1);
GPIO_Pins_Set(GPIOB, GPIO_PIN_11);
do
{
SPI_FLASH_FastRead(Address + Offset + Index, Buffer, sizeof(Buffer));
for(uint8_t i = 0; i < sizeof(Buffer); i++)
{
if(Index++ < Length)
{
LCD_SPI_WriteByte(Buffer[i]);
}
}
}while(Index < Length);
Address = 0x200000;
Offset = 0;
Length = 0x00204C;
Index = 8;
SPI_FLASH_FastRead(Address + Offset, Buffer, 8);
Width = Buffer[2];
Width <<= 8;
Width |= Buffer[3];
Height = Buffer[4];
Height <<= 8;
Height |= Buffer[5];
StartX = (240 - Width ) / 2;
StartY = 255;
LCD_SetAddress(StartX, StartY, StartX + Width - 1, StartY + Height - 1);
GPIO_Pins_Set(GPIOB, GPIO_PIN_11);
do
{
SPI_FLASH_FastRead(Address + Offset + Index, Buffer, sizeof(Buffer));
for(uint8_t i = 0; i < sizeof(Buffer); i++)
{
if(Index++ < Length)
{
LCD_SPI_WriteByte(Buffer[i]);
}
}
}while(Index < Length);
}
WAV相关部分 uint8_t WAV_DecodeFile(WAV_TypeDef *pWav, uint32_t Address)
{
ChunkRIFF_TypeDef *WAV_RIFF;
ChunkFMT_TypeDef *WAV_FMT ;
ChunkFACT_TypeDef *WAV_FACT;
ChunkDATA_TypeDef *WAV_DATA;
uint8_t WAV_HeadBuffer[512];
SPI_FLASH_FastRead(Address, WAV_HeadBuffer, 512);
/* 获取RIFF块 */
WAV_RIFF = (ChunkRIFF_TypeDef *)WAV_HeadBuffer;
/* 是WAV格式文件 */
if(WAV_RIFF->Format == 0x45564157)
{
/* 获取FMT块 */
WAV_FMT = (ChunkFMT_TypeDef *)(WAV_HeadBuffer+12);
/* 读取FACT块 */
WAV_FACT = (ChunkFACT_TypeDef *)(WAV_HeadBuffer+12+8+WAV_FMT->ChunkSize);
if((WAV_FACT->ChunkID == 0x74636166) || (WAV_FACT->ChunkID == 0x5453494C))
{
/* 具有FACT/LIST块的时候(未测试) */
pWav->DataStart=12+8+WAV_FMT->ChunkSize+8+WAV_FACT->ChunkSize;
}
else
{
pWav->DataStart=12+8+WAV_FMT->ChunkSize;
}
/* 读取DATA块 */
WAV_DATA = (ChunkDATA_TypeDef *)(WAV_HeadBuffer+pWav->DataStart);
/* 解析成功 */
if(WAV_DATA->ChunkID == 0x61746164)
{
pWav->AudioFormat = WAV_FMT->AudioFormat; /* 音频格式 */
pWav->nChannels = WAV_FMT->NumOfChannels; /* 通道数 */
pWav->SampleRate = WAV_FMT->SampleRate; /* 采样率 */
pWav->BitRate = WAV_FMT->ByteRate*8; /* 得到位速 */
pWav->BlockAlign = WAV_FMT->BlockAlign; /* 块对齐 */
pWav->BitsPerSample = WAV_FMT->BitsPerSample; /* 位数,16/24/32位 */
pWav->DataSize = WAV_DATA->ChunkSize; /* 数据块大小 */
pWav->DataStart = pWav->DataStart+8; /* 数据流开始的地方 */
printf("\r\npWav->AudioFormat : %d", pWav->AudioFormat);
printf("\r\npWav->nChannels : %d", pWav->nChannels);
printf("\r\npWav->SampleRate : %d", pWav->SampleRate);
printf("\r\npWav->BitRate : %d", pWav->BitRate);
printf("\r\npWav->BlockAlign : %d", pWav->BlockAlign);
printf("\r\npWav->BitsPerSample : %d", pWav->BitsPerSample);
printf("\r\npWav->DataSize : %d", pWav->DataSize);
printf("\r\npWav->DataStart : %d", pWav->DataStart);
WAV_Offset = pWav->DataStart + Address;
WAV_DataSize = pWav->DataSize;
}
}
return 0;
}
void WAV_PrepareData(void)
{
if(WAV_NextIndex == 0)
{
SPI_FLASH_FastRead(WAV_Offset + WAV_TxLength, WAV_DataBuffer[0], WAV_BUFFER_SIZE);
}
else
{
SPI_FLASH_FastRead(WAV_Offset + WAV_TxLength, WAV_DataBuffer[1], WAV_BUFFER_SIZE);
}
WAV_TxLength += WAV_BUFFER_SIZE;
if(WAV_TxLength > WAV_DataSize) WAV_PlayEnded = 1;
}
void WAV_PlayHandler(void)
{
if(WAV_PlayEnded == 0)
{
CS4344_I2S_DMA_Transfer((uint16_t *)&WAV_DataBuffer[WAV_NextIndex][0], (WAV_BUFFER_SIZE / 2));
}
else
{
DMA_Channel_Disable(DMA_CH1);
printf("\r\nWAV Play Finish!\r\n"); WAV_PlayState = 0;
}
if(WAV_NextIndex == 0) WAV_NextIndex = 1;
else WAV_NextIndex = 0;
}
void WAV_PlaySong(void)
{
WAV_TypeDef WaveFile;
if(WAV_PlayState == 0)
{
if(WAV_DecodeFile(&WaveFile, 0x100000) == 0)
{
if((WaveFile.BitsPerSample == 16) && (WaveFile.nChannels == 2) &&
(WaveFile.SampleRate > 44000) && (WaveFile.SampleRate < 48100))
{
WAV_NextIndex = 0;
WAV_PlayEnded = 0;
WAV_TxLength = 0;
WAV_PlayState = 1;
printf("\r\n");
printf("\r\nWAV Data Size : %d, Data Start : 0x%08x", WAV_DataSize, WAV_Offset);
printf("\r\n");
WAV_PrepareData();
WAV_PlayHandler();
}
else
{
printf("\r\nWAV File Format Error!\r\n"); return;
}
}
else
{
printf("\r\nNo WAV File!\r\n"); return;
}
}
else
{
printf("\r\nThe Song Is Not Over Yet!\r\n");
}
}
KEY相关部分 void KEY_Init(void)
{
GPIO_InitType GPIO_InitStructure;
RCC_AHB_Peripheral_Clock_Enable(RCC_AHB_PERIPH_GPIOA);
GPIO_Structure_Initialize(&GPIO_InitStructure);
GPIO_InitStructure.Pin = GPIO_PIN_0;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.GPIO_Pull = GPIO_NO_PULL;
GPIO_Peripheral_Initialize(GPIOA, &GPIO_InitStructure);
GPIO_Structure_Initialize(&GPIO_InitStructure);
GPIO_InitStructure.Pin = GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.GPIO_Pull = GPIO_PULL_UP;
GPIO_Peripheral_Initialize(GPIOA, &GPIO_InitStructure);
TASK_Append(TASK_ID_KEY, KEY_Scan, 10);
}
void KEY_Handler(char *Name, uint8_t State, uint8_t Value)
{
if(strcmp(Name, "WAKEUP") == 0)
{
if(State == 0)
{
CAMERA_TakePhoto();
for(uint16_t i = 1; i <= CAMERA_PacketTotal; i++)
{
CAMERA_GetPacket(i);
}
CAMERA_DrawImage();
#if ENABLE_WAV
WAV_PlaySong();
#endif
}
}
if(strcmp(Name, "KEY3") == 0)
{
if(State == 1)
{
LCD_SetForecolor(FORECOLOR);
LCD_ShowLOG(76, 230, "正在开门...");
}
else
{
LCD_SetForecolor(FORECOLOR);
LCD_ShowLOG(76, 230, " ");
}
}
}
void KEY_SubScan(uint8_t *State, uint8_t *Count, uint8_t Value, uint8_t Active, char *Name)
{
if(*State == 0)
{
if(Value == Active) *Count += 1;
else *Count = 0;
if(*Count > 5)
{
*Count = 0; *State = 1;
printf("\r\n%s Pressed", Name); KEY_Handler(Name, 1, false);
}
}
else
{
if(Value != Active) *Count += 1;
else *Count = 0;
if(*Count > 5)
{
*Count = 0; *State = 0;
printf("\r\n%s Release", Name); KEY_Handler(Name, 0, true);
}
}
}
void KEY_Scan(void)
{
static uint8_t KeyState[4] = {0, 0, 0, 0};
static uint8_t KeyCount[4] = {0, 0, 0, 0};
KEY_SubScan(&KeyState[0], &KeyCount[0], GPIO_Input_Pin_Data_Get(GPIOA, GPIO_PIN_0), PIN_SET, "WAKEUP");
KEY_SubScan(&KeyState[1], &KeyCount[1], GPIO_Input_Pin_Data_Get(GPIOA, GPIO_PIN_4), PIN_RESET, "KEY1");
KEY_SubScan(&KeyState[2], &KeyCount[2], GPIO_Input_Pin_Data_Get(GPIOA, GPIO_PIN_5), PIN_RESET, "KEY2");
KEY_SubScan(&KeyState[3], &KeyCount[3], GPIO_Input_Pin_Data_Get(GPIOA, GPIO_PIN_6), PIN_RESET, "KEY3");
}
运行效果 & 演示视频 https://www.bilibili.com/video/BV1XG4y1i7wr?vd_source=c2d3413b74687345dfc8205a8d735598
附件
其它 在调试SPI接口时,MCU资源只配备了2路SPI,其中一路我们需要当I2S使用,那剩下的那一路就需要驱动SPI FLASH和SPI接口的TFT显示屏,这样就需要不同的CS引脚来控制;我们将CS引脚配置为硬件控制和软件控制方式的时候,代码实现是不同的,在软件控制时需要特别注意,具体参考如下代码: void LCD_InitSPI2(void)
{
GPIO_InitType GPIO_InitStructure;
SPI_InitType SPI_InitStructure;
RCC_APB2_Peripheral_Clock_Enable(RCC_APB2_PERIPH_SPI2);
RCC_AHB_Peripheral_Clock_Enable( RCC_AHB_PERIPH_GPIOB);
#if 1
GPIO_Structure_Initialize(&GPIO_InitStructure);
GPIO_InitStructure.Pin = GPIO_PIN_12;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_OUT_PP;
GPIO_Peripheral_Initialize(GPIOB, &GPIO_InitStructure);
GPIO_Structure_Initialize(&GPIO_InitStructure);
GPIO_InitStructure.Pin = GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.GPIO_Alternate = GPIO_AF1_SPI2;
GPIO_Peripheral_Initialize(GPIOB, &GPIO_InitStructure);
#else
GPIO_Structure_Initialize(&GPIO_InitStructure);
GPIO_InitStructure.Pin = GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.GPIO_Alternate = GPIO_AF1_SPI2;
GPIO_Peripheral_Initialize(GPIOB, &GPIO_InitStructure);
#endif
SPI_I2S_Reset(SPI2);
SPI_Initializes_Structure(&SPI_InitStructure);
SPI_InitStructure.DataDirection = SPI_DIR_DOUBLELINE_FULLDUPLEX;
SPI_InitStructure.SpiMode = SPI_MODE_MASTER;
SPI_InitStructure.DataLen = SPI_DATA_SIZE_8BITS;
SPI_InitStructure.CLKPOL = SPI_CLKPOL_LOW;
SPI_InitStructure.CLKPHA = SPI_CLKPHA_FIRST_EDGE;
SPI_InitStructure.NSS = SPI_NSS_SOFT;
SPI_InitStructure.BaudRatePres = SPI_BR_PRESCALER_8;
SPI_InitStructure.FirstBit = SPI_FB_MSB;
SPI_InitStructure.CRCPoly = 7;
SPI_Initializes(SPI2, &SPI_InitStructure);
#if 1
SPI_NSS_Config(SPI2, SPI_NSS_SOFT);
SPI_Set_Nss_Level(SPI2, SPI_NSS_HIGH);
#else
SPI_NSS_Config(SPI2, SPI_NSS_HARD);
SPI_SS_Output_Enable(SPI2);
#endif
SPI_ON(SPI2);
}
另外就是在实现I2S功能时,也特别感谢国民技术和王工的支持!!!
|