一、背景
假期想做一个锁控系统的项目,其中使用F103ZET6为主控,与AS608模块通信。由于AS608模块的指令发送和部分接收的字长都不相等,所以索性就学习了以空闲中断+DMA方式串口模式,分享一下自己踩的小坑,帮助后来者学习。

二、准备
我使用的HAL库,用CubeMX快速配置模式,开启UART1作为串口调试显示接收到电脑,UART3作为与AS608的通信串口。

NVIC什么的乱配一通,

串口DMA采用字节的方式接收和发送,普通模式

三、AS608模块按两次指纹登录一个模板存于flash指纹库代码
1、宏定义
首先我让deepseek给我根据文档生成.h文件所需要的模块宏定义,当然部分还需要自己微调(有了它是真的方便)。
#ifndef __AS60X_CMD_H__
#define __AS60X_CMD_H__
#include "stm32f1xx_hal.h"
#include "usart.h"
#include "led.h"
#include "delay/delay.h"
#include <stdint.h>
//------------------------- 基础定义 -------------------------
#pragma pack(push, 1) // 1字节对齐
// 包头定义
#define AS60X_PACKET_HEADER __REV16(0xEF01)
// 包标识
#define PKG_TYPE_CMD 0x01 // 命令包
#define PKG_TYPE_DATA 0x02 // 数据包
#define PKG_TYPE_END 0x08 // 结束包
#define PKG_TYPE_RESPONSE 0x07 // 应答包
// 缓冲区编号
#define BUFFER_ID_1 0x01 // CharBuffer1
#define BUFFER_ID_2 0x02 // CharBuffer2
// 默认芯片地址
#define DEFAULT_CHIP_ADDR 0xFFFFFFFF
//发送包的最大长度
#define Packet_MAXLEN 30
//------------------------- 指令代码 -------------------------
typedef enum {
CMD_GetImage = 0x01, // 录入图像
CMD_GenChar = 0x02, // 生成特征
CMD_Match = 0x03, // 精确比对
CMD_Search = 0x04, // 搜索指纹
CMD_RegModel = 0x05, // 合并特征生成模板
CMD_StoreChar = 0x06, // 存储模板
CMD_LoadChar = 0x07, // 读取模板
CMD_UpChar = 0x08, // 上传特征
CMD_DownChar = 0x09, // 下载特征
CMD_UpImage = 0x0A, // 上传图像
CMD_DownImage = 0x0B, // 下载图像
CMD_DeletChar = 0x0C, // 删除模板
CMD_Empty = 0x0D, // 清空指纹库
CMD_WriteReg = 0x0E, // 写寄存器
CMD_ReadSysPara = 0x0F, // 读系统参数
CMD_Enroll = 0x10, // 自动注册模板
CMD_Identify = 0x11, // 自动验证指纹
CMD_SetPwd = 0x12, // 设置口令
CMD_VfyPwd = 0x13, // 验证口令
CMD_GetRandomCode = 0x14, // 采样随机数
CMD_SetChipAddr = 0x15, // 设置芯片地址
CMD_ReadINFpage = 0x16, // 读FLASH信息页
CMD_PortControl = 0x17, // 端口控制
CMD_WriteNotepad = 0x18, // 写记事本
CMD_ReadNotepad = 0x19, // 读记事本
CMD_BurnCode = 0x1A, // 烧写FLASH
CMD_HighSpeedSearch = 0x1B, // 高速搜索
CMD_GenBinImage = 0x1C, // 生成二值化图像
CMD_ValidTempleteNum = 0x1D, // 读有效模板数
CMD_UserGPIOCommand = 0x1E, // GPIO控制
CMD_ReadIndexTable = 0x1F, // 读索引表
} AS60x_CommandCode;
//------------------------- 应答确认码 -------------------------
typedef enum {
ACK_OK = 0x00, // 成功
ACK_PACKET_ERROR = 0x01, // 收包错误
ACK_NO_FINGER = 0x02, // 无手指
ACK_IMAGE_FAIL = 0x03, // 录入失败
ACK_IMAGE_DRY = 0x04, // 图像太干
ACK_IMAGE_WET = 0x05, // 图像太湿
// ... 其他确认码参考文档定义
ACK_PWD_ERROR = 0x13, // 口令错误
} AS60x_AckCode;
//------------------------- 数据结构 -------------------------
// 通用指令包结构(示例:CMD_GetImage)
typedef struct {
uint16_t header; // 包头 0xEF01
uint32_t chip_addr; // 芯片地址
uint8_t pkg_type; // 包标识 (0x01)
uint16_t pkg_len; // 包长度(不包含自身)
uint8_t cmd_code; // 指令代码
uint16_t checksum; // 校验和(包标识到校验和的和)
} AS60x_GetImage;
typedef struct {
uint16_t header; // 包头 0xEF01
uint32_t chip_addr; // 芯片地址
uint8_t pkg_type; // 包标识 (0x01)
uint16_t pkg_len; // 包长度(不包含自身)
uint8_t cmd_code; // 指令代码
uint8_t bufferID; // 缓冲区
uint16_t checksum; // 校验和(包标识到校验和的和)
} AS60x_GenChar;
typedef struct {
uint16_t header; // 包头 0xEF01
uint32_t chip_addr; // 芯片地址
uint8_t pkg_type; // 包标识 (0x01)
uint16_t pkg_len; // 包长度(不包含自身)
uint8_t cmd_code; // 指令代码
uint8_t bufferID; // 缓冲区
uint16_t pageID; //位置号
uint16_t checksum; // 校验和(包标识到校验和的和)
} AS60x_StoreChar;
// 通用应答包结构
typedef struct {
uint16_t header; // 包头 0xEF01
uint32_t chip_addr; // 芯片地址
uint8_t pkg_type; // 包标识 (0x07)
uint16_t pkg_len; // 包长度
uint8_t ack_code; // 确认码
uint16_t checksum; // 校验和
} AS60x_ResponsePacket;
//------------------------- 寄存器定义 -------------------------
typedef enum {
REG_BAUD_RATE = 0x04, // 波特率控制寄存器
REG_SECURITY_LEVEL = 0x05, // 安全等级寄存器
REG_PACKET_SIZE = 0x06, // 数据包大小寄存器
} AS60x_RegAddr;
extern uint8_t AS608_Buffer[Packet_MAXLEN];
extern uint8_t Flag;
void SendCMD_GetImage(void);
void SendCMD_GenChar(uint8_t buffer_ID);
void SendCMD_RegModel(void);
void SendCMD_StoreChar(uint8_t buffer_ID , uint16_t page_ID);
//BSP
uint8_t Log_AS60x(uint16_t page_ID);
#pragma pack(pop)
#endif // __AS60X_CMD_H__
2、.c执行文件
这里我使用串口传递结构体的方式(这也是我从学弟那里得知的,以前都是裤裤传递uint8数组),感觉这样才更方便阅读和管理。将结构体以二进制数据性进行传递,将结构体的内存布局视为一个连续的字节数组,以便通过逐字节的方式访问或传输数据。在调用类似HAL_UART_Transmit_DMA的函数时,需要传递一个uint8_t* 类型的缓冲区指针。通过强制转换,可以直接发送结构体的原始内存。

由于STM32是以小端存储数据,内存的读写永远从低地址开始读/写,比如存储0x0006,在其内部就是显示就是0x0600,串口传输的也是0x0600,而AS608模块要接收这位数据0x0006才有响应,所以就要把STM内部存储的那几位数据进行按字节反转,使用__REV16()函数,反转两个字节。在其他函数中我直接写入手动反转😂,懒得挨个写入函数了。

在编写Log_AS60x()时,一定要注意两个指令之间的时间间隔,我这里设置的都是3s,如果过短的时间间隔会导致取指失败。根据AS608模块的用户手册写业务逻辑,代码全文如下:

#include "AS608.h"
uint8_t AS608_Buffer[Packet_MAXLEN];
AS60x_GetImage GetImage_CommandPacket;
AS60x_GenChar GenChar_CommandPacket;
AS60x_GetImage RegModel_CommandPacket;
AS60x_StoreChar StoreChar_CommandPacket;
uint8_t Flag = 0;
void SendCMD_GetImage(void)
{
GetImage_CommandPacket.header = AS60X_PACKET_HEADER;
GetImage_CommandPacket.chip_addr = DEFAULT_CHIP_ADDR;
GetImage_CommandPacket.pkg_type = 0x01;
GetImage_CommandPacket.pkg_len = 0x0300;
GetImage_CommandPacket.cmd_code = CMD_GetImage;
GetImage_CommandPacket.checksum = 0x0500;
if(HAL_UART_GetState(&huart3) != HAL_UART_STATE_BUSY_TX){
HAL_UART_Transmit_DMA(&huart3,(uint8_t*)&GetImage_CommandPacket,sizeof(AS60x_GetImage));
// printf("1\r\n");
}
}
void SendCMD_GenChar(uint8_t buffer_ID)
{
GenChar_CommandPacket.header = AS60X_PACKET_HEADER;
GenChar_CommandPacket.chip_addr = DEFAULT_CHIP_ADDR;
GenChar_CommandPacket.pkg_type = 0x01;
GenChar_CommandPacket.pkg_len = 0x0400;
GenChar_CommandPacket.cmd_code = CMD_GenChar;
GenChar_CommandPacket.bufferID = buffer_ID;
if(buffer_ID == 0x01){
GenChar_CommandPacket.checksum = 0x0800;
}else if(buffer_ID == 0x02){
GenChar_CommandPacket.checksum = 0x0900;
}
if(HAL_UART_GetState(&huart3) != HAL_UART_STATE_BUSY_TX){
HAL_UART_Transmit_DMA(&huart3,(uint8_t*)&GenChar_CommandPacket,sizeof(AS60x_GenChar));
// printf("2\r\n");
}
}
void SendCMD_RegModel(void)
{
RegModel_CommandPacket.header = AS60X_PACKET_HEADER;
RegModel_CommandPacket.chip_addr = DEFAULT_CHIP_ADDR;
RegModel_CommandPacket.pkg_type = 0x01;
RegModel_CommandPacket.pkg_len = 0x0300;
RegModel_CommandPacket.cmd_code = CMD_RegModel;
RegModel_CommandPacket.checksum = 0x0900;
HAL_UART_Transmit_DMA(&huart3,(uint8_t*)&RegModel_CommandPacket,sizeof(AS60x_GetImage));
}
void SendCMD_StoreChar(uint8_t buffer_ID , uint16_t page_ID)
{
StoreChar_CommandPacket.header = AS60X_PACKET_HEADER;
StoreChar_CommandPacket.chip_addr = DEFAULT_CHIP_ADDR;
StoreChar_CommandPacket.pkg_type = 0x01;
StoreChar_CommandPacket.pkg_len = __REV16(0x06); //反转
StoreChar_CommandPacket.cmd_code = 0x06;
StoreChar_CommandPacket.bufferID = buffer_ID;
StoreChar_CommandPacket.pageID = __REV16(page_ID);
StoreChar_CommandPacket.checksum = __REV16(StoreChar_CommandPacket.pkg_type + __REV16(StoreChar_CommandPacket.pkg_len)
+ StoreChar_CommandPacket.cmd_code + StoreChar_CommandPacket.bufferID + __REV16(StoreChar_CommandPacket.pageID));
HAL_UART_Transmit_DMA(&huart3,(uint8_t*)&StoreChar_CommandPacket,sizeof(AS60x_StoreChar));
}
uint8_t Log_AS60x(uint16_t page_ID)
{
SendCMD_GetImage();
delay_ms(3000);
if(AS608_Buffer[9] == 0x00){
SendCMD_GenChar(0x01);
delay_ms(3000);
}else
return 0;
SendCMD_GetImage();
delay_ms(3000);
if(AS608_Buffer[9] == 0x00){
SendCMD_GenChar(0x02);
delay_ms(3000);
}else
return 0;
SendCMD_RegModel();
delay_ms(3000);
SendCMD_StoreChar(0x02,page_ID);
delay_ms(3000);
return 1;
}
3.中断回调函数文件
我将中断回调放入了一个自己创建的文件里, 看起来更简洁些(也许)。重写HAL_UARTEx_RxEventCallback函数,HAL_UART_TxCpltCallback函数。其中HAL_UARTEx_ReceiveToIdle_DMA()函数结合DMA和空闲中断。无需依赖固定长度或结束符,通过总线空闲自动判断帧结束,而空闲时间由UART波特率决定(例如:9600bps时,1字节传输时间为1.04ms,空闲时间至少为1字节时间)。
开启这个函数会同时开启两个中断 DMA_IT_HT(DMA半传输中断位)和 DMA_IT_TC( DMA传输完成中断)。
由于开起了HAL_UARTEx_ReceiveToIdle_DMA()后直接使用的话,就会传输半个长度的数据便检测,然后全检测,几乎同一时间就会检测两遍,实际效果如下:

为避免半传输中断的产生,我就在其后面加入__HAL_DMA_DISABLE_IT(&hdma_usart3_rx,DMA_IT_HT);

在HAL_UART_TxCpltCallback()函数中使用HAL_UARTEx_ReceiveToIdle_DMA()回调HAL_UARTEx_RxEventCallback()函数。把内容发送给UART1串口验证

手指头按上时的验证结果如下:

不按上时的验证结果如下:

手指第一次取指纹放上去第二次不放上去结果如下:

回调函数代码全文如下:
#include "CpltCallback/CpltCallback.h"
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart->Instance == USART1){
}
if(huart->Instance == USART3){
HAL_UART_Transmit_DMA(&huart1,AS608_Buffer,Size);
if(AS608_Buffer[9] == 0x00){
printf("\r\n");
}else{
printf("\r\n");
}
HAL_UARTEx_ReceiveToIdle_DMA(huart,AS608_Buffer,Size);
__HAL_DMA_DISABLE_IT(&hdma_usart3_rx,DMA_IT_HT);
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1){
}
if(huart->Instance == USART3){
HAL_UARTEx_ReceiveToIdle_DMA(huart,AS608_Buffer,30);
__HAL_DMA_DISABLE_IT(&hdma_usart3_rx,DMA_IT_HT);
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART3){
if(AS608_Buffer[9] == 0x00){
printf("success\r\n");
}else{
printf("fail\r\n");
}
while(HAL_UART_Receive_DMA(huart,AS608_Buffer,12) != HAL_OK);
}
}
四、结语
AS608模块中有很多的指令,可玩性还是很高的,并且可以作为练习串口传输的工具,了解串口的各种模式。
今天考研公布分数,今年的国家线比去年下降了十几分,但我没有过国家线,还是那段时间没有真正脚踏实地的学习。很难过虽然考完的当天就感觉没戏,觉得这半年时间里是浪费掉的。开学后就要找实习工作去,希望在工作这方面不要浮于表面,踏实专研。
3996

被折叠的 条评论
为什么被折叠?



