实验目的
项目背景
希望做一款舵机复现手臂运动的桌面装置,分为穿戴端和桌面端。
穿戴端计划采用单片机收集2个陀螺仪数据,通过无线收发装置(验证版采用zigbee透传模块,后续考虑接入局域网控制)。
本实验目的为开发穿戴端的收发程序。
开发环境选择
网购了两块开发板,商家商品标注不明,实际使用的是国产的pin-to-pin兼容芯片。但这也导致难以使用Cube IDE进行开发(烧写会报错“ Could not verify ST device! Abort connection.”有教程进行跳过:ST-Link设备连接。 Could not verify ST device! Abort connection._error in final launch sequence: failed to start gd-CSDN博客)。不过笔者电脑未安装STM32 ST-LINK Utility,遂作罢。选择Keil 5开发。
实验过程
SDK安装
芯片是Geehy的,公司官网:珠海极海半导体有限公司 (geehy.com)
SDK下载地址:珠海极海半导体有限公司 | APM32F103系列 (geehy.com)(向下拉,选择APM32F10x_SDK 与 APM32F1xx_DFP Pack 下载,前者是代码库,后者是Keil Pack开发板资源)
芯片数据手册:APM32F103xC数据手册 V1.7.pdf (geehy.com)
我使用的不是官方推荐的APM32F103VC MINI开发板(使用手册:APM32F103VC MINI开发板使用说明书V1.1.pdf (geehy.com)),而是下面这款商品:https://m.tb.cn/h.gjxTn2heTm3fWv3?tk=d4Uo32ljgY6
实拍照片:
缺少部分外设,不过不影响使用官网软件开发包进行开发。
环境配置
先双击安装开发板资源。
为了节省时间,我选择直接基于部分例程(APM32F10x_SDK_V1.8\Examples\GPIO\GPIO_Toggle)进行开发。后续有时间自己再重走一遍。
程序烧写采用ST-LINK连接(菜单栏:Flash -> Configure Flash Tools... -> Debug -> 下图标注处,选择连接方式)。
代码编写
通过Keil 5打开项目文件夹APM32F10x_SDK_V1.8\Examples\GPIO\GPIO_Toggle\Project\MDK\GPIO_Toggle.uvprojx,进入项目编辑。
点灯(GPIO输出)
修改 Board_APM32F103_MINI.h 中的部分宏定义以适配开发板。开发板载LED(除电源指示LED)引脚PC13。找到相关代码,修改引脚(代码为修改后):
/** @addtogroup APM32F103_MINI_LED
* @{
*/
#define LEDn 2
#define LED2_PIN GPIO_PIN_13
#define LED2_GPIO_PORT GPIOC
#define LED2_GPIO_CLK RCM_APB2_PERIPH_GPIOC
然后就可以在main函数(所有#include "Board_APM32F103_MINI\inc\Board_APM32F103_MINI.h"的文件)中调用以下函数:
void APM_MINI_LEDInit(Led_TypeDef Led);
void APM_MINI_LEDOn(Led_TypeDef Led);
void APM_MINI_LEDOff(Led_TypeDef Led);
void APM_MINI_LEDToggle(Led_TypeDef Led);
操作LED2了。同样地,也可以将LED3重定义为某些与LED操作方式一致的外设。
串口回响
在打开DMA前,先实现串口回响(收什么发什么)。
新增文件uart_driver.h、uart_driver.c,撰写串口相关函数。注意新增的头文件要放入指定的文件夹(如本项目中的APM32F10x_SDK_V1.8\Examples\GPIO\GPIO_Toggle\Include),如果想自定义头文件需要自行添加目录(菜单栏:Flash -> Configure Flash Tools... -> C/C++ -> 下图标注处,添加路径)。
在uart_driver.c中定义USART1的初始化函数、中断服务函数与收发函数:
// 文件:uart_driver.c
// 功能:串口回响
#include <stdint.h>
#include "apm32f10x_usart.h"
// #include "apm32f10x_dma.h" //未开启DMA时不需要
#include "Board_APM32F103_MINI\inc\Board_APM32F103_MINI.h"
#include "uart_driver.h"
// USART1 TEST 初始化
// 非DMA版本
void USART1_Init()
{
// 使用结构体初始化串口USART1
USART_Config_T U1_Configer;
U1_Configer.baudRate = 9600; // 波特率,9600
U1_Configer.wordLength = USART_WORD_LEN_8B; // 数据位,8bit
U1_Configer.stopBits = USART_STOP_BIT_1; // 停止位,1bit
U1_Configer.parity = USART_PARITY_NONE; //校验位,0bit
U1_Configer.mode = USART_MODE_TX_RX; // 模式,收发
U1_Configer.hardwareFlow = USART_HARDWARE_FLOW_NONE; // 硬件流控制,无
APM_MINI_COMInit(COM1, &U1_Configer); // 这是SDK自带的函数,包含GPIO初始化及串口设置
__NVIC_EnableIRQ(USART1_IRQn); // 与STM32不同的函数
USART_EnableInterrupt(USART1, USART_INT_RXBNE); // 开启串口1的接收缓冲区非空中断(RXBNE,与STM的RBNE不同)
}
//
// USART1接收中断处理函数
void USART1_IRQHandler(void)
{
if (USART_ReadIntFlag(USART1, USART_INT_RXBNE) != RESET)
{
// 读取接收到的数据
uint16_t data[2] = {USART_RxData(USART1), 0};
// 发送数据回显
USART1_Send_Str(data);
// 清除接收中断标志位
USART_ClearIntFlag(USART1, USART_INT_RXBNE);
// 重新开启中断
USART_EnableInterrupt(USART1, USART_INT_RXBNE);
}
}
//
// 串口发送字符串函数
void USART1_Send_Str(uint8_t* str)
{
uint8_t cnt = 0;
while (*str && cnt <= USART1_TX_MAX_LEN) {
cnt ++;
USART_TxData(USART1, *str++);
while (USART_ReadStatusFlag(USART1, USART_FLAG_TXC) == RESET) {
// 等待发送完成
}
}
}
在uart_driver.h文件中声明相关函数:
// 文件:uart_driver.h
// 功能:串口回响
#ifndef _UART_DRIVER_H_
#define _UART_DRIVER_H_
#include "apm32f10x_usart.h"
void USART1_Init(void);
void USART1_IRQHandler(void);
void USART1_Send_Str(uint8_t* str);
#endif
main.c中#include "uart_driver.h",main函数:
int main(void)
{
// ==================== 外设初始化 ====================
APM_MINI_LEDInit(LED2);
APM_MINI_LEDOff(LED2);
USART1_Init();
USART1_DMA_Init();
while (1)
{
// ==================== 发送测试 ====================
USART_TxData(USART1, 'X');
Delay();
}
测试效果:
串口DMA接收(常规发送)
实现与JY62的交互。该模块可通过串口以定频率(可调节,≤256Hz)持续发送数据帧,并可通过串口发送指令进行设置。详见:JY62产品资料 (yuque.com)
常规发送功能即可满足需求,接收功能有两种解决方案:(1)DMA接收定长数据:开启传输完成中断(DMA_INT_TC),每接收2个数据帧长度触发中断服务,进行数据帧截取与读取;(2)DMA接收不定长数据:开启串口空闲中断,每个数据帧之间中断触发中断服务,进行数据帧截取与读取。
这里先选择实现第一个方案,第二个后续找时间实现。根据资料,USART1 RX 对应的DMA通道为 DMA1 Channel5。在uart_driver.c中定义相关的初始化函数、中断服务函数与收发函数:
// 文件:uart_driver.c
// 功能:串口1 DMA定长消息接收
#include <stdint.h>
#include <stdio.h>
// ================================================== Std Periph Driver ==================================================
#include "apm32f10x_usart.h"
#include "apm32f10x_dma.h"
#include "apm32f10x_misc.h"
#include "Board_APM32F103_MINI\inc\Board_APM32F103_MINI.h"
#include "uart_driver.h"
#include "pwm_driver.h"
#include "jy62.h" // 我自己写的JY62消息解码程序头文件
// MPU_frame_data数组就是上面这个文件定义的extern变量
#include "main.h"
uint8_t USART1_RX_Buffer[64] = {0};
#define USART1_RX_BUFFER_SIZE 64
#define USART1_TX_MAX_LEN 32
// USART1 DMA接收 初始化
void USART1_DMA_Init(void)
{
// 开启相关外设时钟
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOA | RCM_APB2_PERIPH_GPIOA);
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_USART1);
RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA1);
// GPIO初始化
GPIO_Config_T GPIO_configStruct;
/* Configure USART Tx as alternate function push-pull */
GPIO_configStruct.mode = GPIO_MODE_AF_PP;
GPIO_configStruct.pin = GPIO_PIN_9;
GPIO_configStruct.speed = GPIO_SPEED_50MHz;
GPIO_Config(GPIOA, &GPIO_configStruct);
/* Configure USART Rx as input floating */
GPIO_configStruct.mode = GPIO_MODE_IN_FLOATING;
GPIO_configStruct.pin = GPIO_PIN_10;
GPIO_Config(GPIOB, &GPIO_configStruct);
// 初始化串口USART1
USART_Config_T U1_Configer;
U1_Configer.baudRate = 9600; // 波特率,9600
U1_Configer.wordLength = USART_WORD_LEN_8B; // 数据位,8bit
U1_Configer.stopBits = USART_STOP_BIT_1; // 停止位,1bit
U1_Configer.parity = USART_PARITY_NONE; //校验位,0bit
U1_Configer.mode = USART_MODE_TX_RX; // 模式,收发
U1_Configer.hardwareFlow = USART_HARDWARE_FLOW_NONE; // 硬件流控制,无
/* USART configuration */
USART_Config(USART1, &U1_Configer);
/* Enable USART */
USART_EnableRx(USART1);
USART_EnableTx(USART1);
USART_Enable(USART1);
// 配置 DMA
DMA_Config_T DMA_ConfigStructure;
DMA_ConfigStructure.peripheralBaseAddr = (uint32_t)/*(USART1->DATA)*/ (&(USART1->DATA_B)); // 外设地址
DMA_ConfigStructure.memoryBaseAddr = (uint32_t)USART1_RX_Buffer; // 内存地址
DMA_ConfigStructure.dir = DMA_DIR_PERIPHERAL_SRC; // 外设作为数据源
DMA_ConfigStructure.bufferSize = USART1_RX_BUFFER_SIZE; // 缓冲区大小
DMA_ConfigStructure.peripheralInc = DMA_PERIPHERAL_INC_DISABLE; // 外设地址不增
DMA_ConfigStructure.memoryInc = DMA_MEMORY_INC_ENABLE; // 内存地址增
DMA_ConfigStructure.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_BYTE; // 外设数据宽度为8位
DMA_ConfigStructure.memoryDataSize = DMA_MEMORY_DATA_SIZE_BYTE; // 内存数据宽度为8位
DMA_ConfigStructure.loopMode = DMA_MODE_NORMAL; // 正常模式
DMA_ConfigStructure.priority = DMA_PRIORITY_HIGH; // 高优先级
DMA_ConfigStructure.M2M = DMA_M2MEN_DISABLE; // 禁用内存到内存传输
DMA_Reset(DMA1_Channel5);
DMA_Config(DMA1_Channel5, &DMA_ConfigStructure); // 初始化DMA1通道
DMA_ConfigDataNumber(DMA1_Channel5, 22); // 设置传输数据长度
// 配置 DMA 中断
NVIC_EnableIRQRequest(DMA1_Channel5_IRQn, 0, 0); // 注册中断服务
DMA_EnableInterrupt(DMA1_Channel5, DMA_INT_TC);
// APM32的TC(transmit complete)中断相当于STM32的FTF(full transmit finish)中断
// 这里注意不要把标志位 DMA1_FLAG_INT_TC5 和中断类型 DMA_INT_TC 搞混
// 初始化后初次使能USART1 DMA接收
USART_EnableDMA(USART1, USART_DMA_RX);
DMA_Enable(DMA1_Channel5);
USART1_DMA_Receive();
}
//
// 重启USART1 DMA接收函数
void USART1_DMA_Receive()
{
// 使能 DMA 通道
DMA_ConfigDataNumber(DMA1_Channel5, 22);
DMA_Enable(DMA1_Channel5);
// 配置USART1的DMA接收
USART_EnableDMA(USART1, USART_DMA_RX);
}
//
// DMA接收中断处理函数
void DMA1_Channel5_IRQHandler(void)
{
// 解码
if(DMA_ReadIntFlag(DMA1_INT_FLAG_TC5))
{
// 清除中断标志
DMA_ClearIntFlag(DMA1_INT_FLAG_TC5);
// 去使能 DMA
DMA_Disable(DMA1_Channel5);
// 数据帧处理,从两数据帧长的接收信息中截取0x55开头的消息
// 保证接收到,但牺牲了一半的数据频率
uint8_t i = 0;
while(i + 10 < 22 && USART1_RX_Buffer[i] != 0x55)
{ i++;}
uint8_t ii = 0;
for(ii = 0; ii<10;ii++)
{
MPU_frame_data[ii] = USART1_RX_Buffer[i + ii];
}
MPU_frame_dataRecord(); // from JY62.h
// 重使能 DMA
USART1_DMA_Receive();
}
}
//
// 串口1发送字符串函数
void USART1_Send_Str(uint8_t* str)
{
uint8_t cnt = 0;
while (*str && cnt <= USART1_TX_MAX_LEN) {
cnt ++;
USART_TxData(USART1, *str++);
while (USART_ReadStatusFlag(USART1, USART_FLAG_TXC) == RESET) {
// 等待发送完成
}
}
}
//
类似地在在uart_driver.h文件中声明相关函数。在main函数中加入数据回传、在中断服务函数中加入LED点亮函数,测试是否正常工作符合预期:
// 文件:main.c
// 函数:int main(void)
// 功能:隔时发送距中断剩余传输字节数
int main(void)
{
// ================================================== 外设初始化 ==================================================
APM_MINI_LEDInit(LED2);
APM_MINI_LEDOff(LED2);
USART1_DMA_Init();
while (1)
{
// ================================================== 收发测试 ==================================================
// 测试DMA传输数据cnt剩余量,正常应随字节接收从22到1后重置为22
sprintf(txt, " %d \n", DMA_ReadDataNumber(DMA1_Channel5));
USART1_Send_Str(txt);
Delay();
}
}
定时发送字节,测试效果如下,符合预期:
测试:隔时回传JY62姿态角
先仿照USART1的常规收发代码,初始化USART2作为数据回传串口。
// 文件:uart_driver.c
// 功能:串口1 DMA定长消息接收;串口2 常规收发,数据回传
#include <stdint.h>
#include <stdio.h>
// ================================================== Std Periph Driver ==================================================
#include "apm32f10x_usart.h"
#include "apm32f10x_dma.h"
#include "apm32f10x_misc.h"
#include "Board_APM32F103_MINI\inc\Board_APM32F103_MINI.h"
#include "uart_driver.h"
#include "pwm_driver.h"
#include "jy62.h"
#include "main.h"
uint8_t USART1_RX_Buffer[64] = {0};
#define USART1_RX_BUFFER_SIZE 64
#define USART1_TX_MAX_LEN 32
#define USART2_TX_MAX_LEN 32
// USART1 DMA接收 初始化
void USART1_DMA_Init(void)
{
// 开启相关外设时钟
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOA | RCM_APB2_PERIPH_GPIOA);
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_USART1);
RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA1);
// GPIO初始化
GPIO_Config_T GPIO_configStruct;
/* Configure USART Tx as alternate function push-pull */
GPIO_configStruct.mode = GPIO_MODE_AF_PP;
GPIO_configStruct.pin = GPIO_PIN_9;
GPIO_configStruct.speed = GPIO_SPEED_50MHz;
GPIO_Config(GPIOA, &GPIO_configStruct);
/* Configure USART Rx as input floating */
GPIO_configStruct.mode = GPIO_MODE_IN_FLOATING;
GPIO_configStruct.pin = GPIO_PIN_10;
GPIO_Config(GPIOB, &GPIO_configStruct);
// 初始化串口USART1
USART_Config_T U1_Configer;
U1_Configer.baudRate = 9600; // 波特率,9600
U1_Configer.wordLength = USART_WORD_LEN_8B; // 数据位,8bit
U1_Configer.stopBits = USART_STOP_BIT_1; // 停止位,1bit
U1_Configer.parity = USART_PARITY_NONE; //校验位,0bit
U1_Configer.mode = USART_MODE_TX_RX; // 模式,收发
U1_Configer.hardwareFlow = USART_HARDWARE_FLOW_NONE; // 硬件流控制,无
/* USART configuration */
USART_Config(USART1, &U1_Configer);
/* Enable USART */
USART_EnableRx(USART1);
USART_EnableTx(USART1);
USART_Enable(USART1);
// 配置 DMA
DMA_Config_T DMA_ConfigStructure;
DMA_ConfigStructure.peripheralBaseAddr = (uint32_t)/*(USART1->DATA)*/ (&(USART1->DATA_B)); // 外设地址
DMA_ConfigStructure.memoryBaseAddr = (uint32_t)USART1_RX_Buffer; // 内存地址
DMA_ConfigStructure.dir = DMA_DIR_PERIPHERAL_SRC; // 外设作为数据源
DMA_ConfigStructure.bufferSize = USART1_RX_BUFFER_SIZE; // 缓冲区大小
DMA_ConfigStructure.peripheralInc = DMA_PERIPHERAL_INC_DISABLE; // 外设地址不增
DMA_ConfigStructure.memoryInc = DMA_MEMORY_INC_ENABLE; // 内存地址增
DMA_ConfigStructure.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_BYTE; // 外设数据宽度为8位
DMA_ConfigStructure.memoryDataSize = DMA_MEMORY_DATA_SIZE_BYTE; // 内存数据宽度为8位
DMA_ConfigStructure.loopMode = DMA_MODE_NORMAL; // 正常模式
DMA_ConfigStructure.priority = DMA_PRIORITY_HIGH; // 高优先级
DMA_ConfigStructure.M2M = DMA_M2MEN_DISABLE; // 禁用内存到内存传输
DMA_Reset(DMA1_Channel5);
DMA_Config(DMA1_Channel5, &DMA_ConfigStructure); // 初始化DMA1通道5
DMA_ConfigDataNumber(DMA1_Channel5, 22);
// 配置 DMA 中断
NVIC_EnableIRQRequest(DMA1_Channel5_IRQn, 0, 0);
DMA_EnableInterrupt(DMA1_Channel5, DMA_INT_TC);
// 使能USART1 DMA接收
USART_EnableDMA(USART1, USART_DMA_RX);
DMA_Enable(DMA1_Channel5);
USART1_DMA_Receive();
}
//
// 重启USART1 DMA接收函数
void USART1_DMA_Receive()
{
// 使能 DMA 通道
DMA_ConfigDataNumber(DMA1_Channel5, 22);
DMA_Enable(DMA1_Channel5);
// 配置USART1的DMA接收
USART_EnableDMA(USART1, USART_DMA_RX);
}
//
// DMA接收中断处理函数
void DMA1_Channel5_IRQHandler(void)
{
//debug
APM_MINI_LEDOn(LED2);
// 解码
if(DMA_ReadIntFlag(DMA1_INT_FLAG_TC5))
{
// 清除中断标志
DMA_ClearIntFlag(DMA1_INT_FLAG_TC5);
// 去使能 DMA
DMA_Disable(DMA1_Channel5);
// 数据帧处理
uint8_t i = 0;
while(i + 10 < 22 && USART1_RX_Buffer[i] != 0x55)
{ i++;}
uint8_t ii = 0;
for(ii = 0; ii<11; ii++)
{
MPU_frame_data[ii] = USART1_RX_Buffer[i + ii];
}
MPU_frame_dataRecord();
// 重使能 DMA
USART1_DMA_Receive();
}
}
//
// 串口1发送字符串函数
void USART1_Send_Str(uint8_t* str)
{
uint8_t cnt = 0;
while (*str && cnt <= USART1_TX_MAX_LEN) {
cnt ++;
USART_TxData(USART1, *str++);
while (USART_ReadStatusFlag(USART1, USART_FLAG_TXC) == RESET) {
// 等待发送完成
}
}
}
//
// USART2 初始化
void USART2_Init()
{
// 使用结构体初始化串口USART2
USART_Config_T U2_Configer;
//NVIC_InitTypeDef NVIC_InitStructure;
U2_Configer.baudRate = 9600; // 波特率,9600
U2_Configer.wordLength = USART_WORD_LEN_8B; // 数据位,8bit
U2_Configer.stopBits = USART_STOP_BIT_1; // 停止位,1bit
U2_Configer.parity = USART_PARITY_NONE; //校验位,0bit
U2_Configer.mode = USART_MODE_TX_RX; // 模式,收发
U2_Configer.hardwareFlow = USART_HARDWARE_FLOW_NONE; // 硬件流控制,无
APM_MINI_COMInit(COM2, &U2_Configer);
NVIC_EnableIRQRequest(USART2_IRQn, 0, 0);
USART_EnableInterrupt(USART2, USART_INT_RXBNE);
}
//
// USART1接收中断服务函数
void USART2_IRQHandler(void)
{
if (USART_ReadIntFlag(USART2, USART_INT_RXBNE) != RESET)
{
// 读取接收到的数据
uint16_t data = USART_RxData(USART2);
// 这里添加服务
// 清除接收中断标志位
USART_ClearIntFlag(USART2, USART_INT_RXBNE);
// 重新开启中断
USART_EnableInterrupt(USART2, USART_INT_RXBNE);
}
}
//
// 串口2发送字符串函数
void USART2_Send_Str(uint8_t* str)
{
uint8_t cnt = 0;
while (*str && cnt <= USART2_TX_MAX_LEN) {
cnt ++;
USART_TxData(USART2, *str++);
while (USART_ReadStatusFlag(USART2, USART_FLAG_TXC) == RESET) {
// 等待发送完成
}
}
}
//
main函数添加相关代码:
// 文件:main.c
// 函数:int main(void)
// 功能:隔时回传JY62姿态角
int main(void)
{
// ================================================== 外设初始化 ==================================================
APM_MINI_LEDInit(LED2);
APM_MINI_LEDOff(LED2);
USART2_Init();
USART1_DMA_Init();
while (1)
{
// ================================================== 数据回传测试 ==================================================
// 结构体来自jy62.h
sprintf(txt, "r %f ", Angle.roll);
USART2_Send_Str(txt);
Delay();
sprintf(txt, "p %f ", Angle.pitch);
USART2_Send_Str(txt);
Delay();
sprintf(txt, "y %f ", Angle.yaw);
USART2_Send_Str(txt);
Delay();
}
}
实验效果:
实验总结
- 熟悉了APM32F10x的开发环境配置与库文件定义
- 复现了常规外设(GPIO,USART,DMA)操作
- 完成了部分模块的驱动移植,为下一步开发做准备
下次实验计划进行定时器TMR与PWM输出的驱动,以实现呼吸灯与舵机控制。
其他感想
第一次写博客,也是在对以往实验记录方式进行了反思之后做出的决定。以后会更多地把这些个人的小实验作线上记录,给自己一个动力,也希望能吸引到同好交流与指教。
APM32作为国产 Arm Cortex-M3 架构单片机的优秀代表之一,单片机性能不错,价格也比较实惠,对于个人开发者来说性价比良好。美中不足的是SDK提供的库文件与STM32的表述区别还是很多的,移植并没有那么容易;且部分注释仍然不够充足、清晰。不过充足的示例还是为学习提供了便利。以后会多考虑。