自我介绍:东莞社区工作人员,日常工作为运维。对单片机有兴趣。
硬件介绍:MAX32660-EVSYS 美信出品。Arm Cortex-M4F内核, 工作频率96MHz; 256KB Flash Memory; 96KB SRAM; 16KB 指令缓存。有14路GPIO.两路SPI,两路IIC,两路串口。支持低功耗。MAX32660-EVSYS开发板自带DAP swd调试器,一路LED,一个按键。支持面包板。,
应用场景:健身监测器 ; 工业传感器 ; IoT ; 便携式医疗设备; 运动手表可穿戴医疗设备。
设计思路:按照活动要求,使用MAX32660-EVSYS开发板完成时钟和计步器功能。针对时钟功能,此开发板自带RTC功能,但是没搞明白怎么外接电池。如果不能外接电池,断电后时间信息就完全丢失了,所以额外找了个DS3232模块,做为时钟模块。时钟模块DS3232与开发板之间用IIC连接。时钟模块自带电池,可以保证时间信息不丢失。
计步器使用MPU6050。MPU6050是一个9轴运动处理传感器。它集成了3轴MEMS陀螺仪,3轴MEMS加速度计,以及一个可扩展的数字运动处理器DMP(Digital Motion Processor)。使用IIC与开发板通讯。这款传感器网上资料非常多,并且在DMP功能中就集成了计步功能,原子的教程中有完整的例程和讲解。
显示部分用OLED来做显示。听了硬禾的直播,看有用墨水屏来做展示的,很羡慕,但是手头没有设备,就此作罢。
有了设计思路,遇到第一个问题:DS3232与MPU6050都是用IIC通讯,这本不是问题,但是这两个模块的IIC地址都是0x68,这就让人抓狂了,用一组IIC,无法区别两个设备。所以只能将这两个设备挂在不同的IIC上了。但是这个开发板的spi和iic1(P00,P01)使用的是同一组管脚,如果使用SPI1(p0_10,p0_11)就要用到串口1,而串口1 是接到DAP上了,如果要用,就要掰开DAP,那样会导致以后的烧写困难。权衡后,放弃使用SPI的oled,改用IIC的oled,借来了一块主控SSD1307Z的0.65寸的OLED屏幕。这样DS3232接在IIC1(p0_0,p0_1),mpu6050和oled接在IIC0(p0_9,p0_8)上。再编写一个上位机,用来和网络校正时间。上位机与开发板通过串口1通讯。上位机就用python来做,简单易懂。
第二个问题:串口问题。这个开发板提供了例程,例程中有个函数printf可以直接通过串口将信息输出到电脑上去。也就是说开发板的例程是初始化了串口1的。直接添加串口中断函数
void UART1_IRQHandler(void)
经过测试,发现完全没有调用到函数。也就是意味着,例程的串口初始化,没有初始化串口接收中断。仔细看例程,又参考了网上的说明。才明白需要在程序中将已经初始化过的串口1,关闭,再重新初始化一次,增加接收中断。
//串口1 初始化
void UART1A_INIT(void){
UART_Shutdown(MXC_UART_GET_UART(1)); //关闭串口1 系统在启动前就初始化了串口1
/* Setup the interrupt */
NVIC_ClearPendingIRQ(MXC_UART_GET_IRQ(1));
NVIC_DisableIRQ(MXC_UART_GET_IRQ(1));
NVIC_SetPriority(MXC_UART_GET_IRQ(1), 0);
NVIC_EnableIRQ(MXC_UART_GET_IRQ(1));
uart_cfg_t cfg;
cfg.parity = UART_PARITY_DISABLE;
cfg.size = UART_DATA_SIZE_8_BITS;
cfg.stop = UART_STOP_1;
cfg.flow = UART_FLOW_CTRL_DIS;
cfg.pol = UART_FLOW_POL_DIS;
cfg.baud = UART_BAUD;
error = UART_Init(MXC_UART_GET_UART(1), &cfg, &sys_uart_cfg); //开启串口1
read_req.data = rxdata;
read_req.len = BUFF_SIZE;
read_req.callback = read_cb;
//write_req.data = txdata;
//write_req.len = BUFF_SIZE;
//write_req.callback = write_cb;
UART_ReadAsync(MXC_UART_GET_UART(1), &read_req);
}
阅读例程代码,自己对美信例程的理解为,开辟了一个缓冲区“read_req.len = BUFF_SIZE;”当串口接收到了数据,数据长度超过了缓冲区,就会调用函数UART1_IRQHandler,然后调用函数read_req,在函数read_req中可以进行自定义的处理。但是不符合自己的需求。我期望的是建立自己的环形缓冲区,当串口接收到数据后,就进入环形缓冲区,开发板程序判断缓冲区大小,若为上位机下传数据包大小,则接收正确,调用处理数据过程,若大小不对,则接收数据过程有误,丢弃数据。思前想后,将美信的缓冲区设置为1个字符大小,在read_req中将一个字符塞进环形缓冲区中。经测试满足需求^_^.
上位机部分。上位机使用python+QT实现,可以跨平台。上位机功能简单,实现日期、时间、步数的展示。增加了一个与网络时间校时的功能。先说一下串口部分,由于开发板和上位机通讯是使用DAP模块的usb转串口功能。而USB口可能会在程序启动后插入。所以使用一个单独的线程检查串口,当有串口增加时,通过信号通知主界面可以选择串口。
serialLock=QMutex() #创建线程锁
class FindSerial(QThread):
sinFindNewSerialPort = pyqtSignal(str) #定义信号
def __init__(self):
super(FindSerial,self).__init__()
self.workstat=True
self.serialdict={}
def run(self):
while True:
# print(threading.currentThread(),self.workstat)
if self.workstat:
port_list = list(list_ports.comports())
for port in port_list:
#检查 是否在列表中存在
if self.serialdict.get(port[0])==None: #不存在这个端口
serialLock.lock()
self.serialdict[port[0]]=port[1]
serialLock.unlock()
self.sinFindNewSerialPort.emit(port[0]) #发送找到新串口的信号
#比较新旧 端口列表 数目是否相同
if len(port_list)==len(self.serialdict):
#数目相同 无变化
# print('子线程还存在着')
self.sleep(1)
else:
#新旧端口列表有变换 清除旧列表
serialLock.lock()
self.serialdict.clear()
serialLock.unlock()
self.sinFindNewSerialPort.emit('clear')
else:
self.sleep(2)
@pyqtSlot() # 打开串口 关闭串口
def on_pushButtonSerCtl_clicked(self):
if self.ui.pushButtonSerCtl.text().find('打开') >= 0:
try:
# 超时设置,None:永远等待操作,0为立即返回请求结果,其他值为等待超时时间(单位为秒)
# 使用115200波特率
self.ser = serial.Serial(self.ui.comboBoxPort.currentText(),
baudrate=115200, bytesize=8, parity='N',
stopbits=1, timeout=1)
except serial.SerialException:
print('错误', '打开串口出错!')
else:
self.ser.flush() # 刷新缓存
self.ui.pushButtonSerCtl.setText('关闭')
self.ui.pushButtonUpdateTime.setEnabled(True) # 允许网络对时
self.ui.comboBoxPort.setEnabled(False)
self.findSerialPortThread.workstat = False # 让线程停止工作
self.timer.start(40) #刷新界面开始 100ms间隔刷新
else:
self.ser.flush() # 刷新缓存
self.ser.close()
self.ui.pushButtonSerCtl.setText('打开')
self.ui.pushButtonUpdateTime.setEnabled(False)
self.ui.comboBoxPort.setEnabled(True)
self.findSerialPortThread.workstat = True # 让线程工作
self.timer.stop() #停止刷新
系统中增加一个定时器,当串口打开后,每40ms读取一次串口,读到串口数据就在界面上显示。校时按钮若被按下,就通过网络获取当前时间,然后将时间发回给下位机。
先前做上位机打开串口后程序就报错,仔细检查发现是缓冲问题,开发板先插上电脑,就不停地向电脑串口发送数据,在程序启动后清除缓冲就好了。再考虑程序在运行期间,USB口被拔出这个异常,遇到这个异常,直接关闭串口,清除缓冲,关闭刷新,启动寻找串口的线程。
步数监测。这块倒是很容易。网上关于MPU6050的资料非常多。而mpu6050的DMP功能里就自带了步数统计的功能。
dmp_get_pedometer_step_count(&step_count); //得到计步步数
dmp_get_pedometer_walk_time(&walk_time); //得到计步所用时间
直接调用对应的函数,就能获得步数和步行的时间,不过貌似步行的时间不太对。在网上也有很多关于步行的滤波方式,对这块没有做过多的学习。这里需要解决的是IIC通讯接口的模块处理。
uint8_t i2cInit(void){
int error;
const sys_cfg_i2c_t sys_i2c_cfg = NULL; /* No system specific configuration needed. */
I2C_Shutdown(I2C0_MASTER);
I2C_Shutdown(I2C1_MASTER);
if((error = I2C_Init(I2C0_MASTER, I2C_STD_MODE, &sys_i2c_cfg)) != E_NO_ERROR) {
//if((error = I2C_Init(I2C_MASTER, I2C_FAST_MODE, &sys_i2c_cfg)) != E_NO_ERROR) {
printf("Error initializing I2C0. (Error code = %d)\n", error);
return 1;
}
if((error = I2C_Init(I2C1_MASTER, I2C_STD_MODE, &sys_i2c_cfg)) != E_NO_ERROR) {
//if((error = I2C_Init(I2C_MASTER, I2C_FAST_MODE, &sys_i2c_cfg)) != E_NO_ERROR) {
printf("Error initializing I2C1. (Error code = %d)\n", error);
return 1;
}
NVIC_EnableIRQ(I2C0_IRQn);
NVIC_EnableIRQ(I2C1_IRQn);
return 0;
}
美信例程里给了IIC的硬件驱动例程,但是和网上见到的有所区别。对照IIC的时序图,掌握好关键的几点:IIC端口,要选对IIC端口,这里驱动了两个IIC端口,一个是IIC0接mpu6050和OLED,一个是IIC1接DS3232。然后是地址,不同设备对应不同驱动地址。不过mpu6050和DS3232用了同一组地址,所以只能分不同iic端口来驱动了。然后就是寄存器,最后是命令字。搞定了IIC通讯,也就搞定了DS3232,OLED,MPU6050三个外设了。驱动相关的资料还是挺丰富的。
//IIC连续写
//addr:器件地址
//reg:寄存器地址
//len:写入长度
//buf:数据区
//返回值:0,正常
// 其他,错误代码
uint8_t MPU_Write_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf){
uint8_t txdata[16],error;
uint8_t i;
txdata[0]=reg;
for(i=1;i<len+1;i++){
txdata[i]=buf[i-1];
}
error=I2C_MasterWrite(MXC_I2C0, (addr<<1)|0, txdata, len+1, 0);
return error-(len+1);
}
//IIC连续读
//addr:器件地址
//reg:要读取的寄存器地址
//len:要读取的长度
//buf:读取到的数据存储区
//返回值:0,正常
// 其他,错误代码
uint8_t MPU_Read_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf){
uint8_t res;
I2C_MasterWrite(MXC_I2C0, (addr<<1)|0, ®, 1, 0);
//printf("read reg=%x len=%d %d\n",(addr<<1)|0,len,res);
//return I2C_MasterRead(MXC_I2C0, (addr<<1)|1, buf, len, 0);
res=I2C_MasterRead(MXC_I2C0, (addr<<1)|1, buf, len, 0);
//printf("read len=%d\n",res);
return res-len;
}
//IIC写一个字节
//reg:寄存器地址
//data:数据
//返回值:0,正常
// 其他,错误代码
uint8_t MPU_Write_Byte(uint8_t reg,uint8_t data){
uint16_t error;
uint8_t txdata[2];
txdata[0]=reg;
txdata[1]=data;
error=I2C_MasterWrite(MXC_I2C0, MPU_WRITE, txdata, 2, 0);
//printf("IIC write reg=%x echo=%d \n",reg,error);
return error-2;
}
//IIC读一个字节
//reg:寄存器地址
//返回值:读到的数据
uint8_t MPU_Read_Byte(uint8_t reg){
uint8_t data;
data=reg;
I2C_MasterWrite(MXC_I2C0, MPU_WRITE, &data, 1, 0);
//printf("IIC write leng=%d\n",error);
I2C_MasterRead(MXC_I2C0, MPU_READ, &data, 1, 0);
//printf("IIC read leng=%d\n",error);
return data;
}
显示部分。本意是想用SPI接口的0.96寸的OLED屏幕,想参考Thomas的文档,完成u8g2的移植的,但是这个开发板GPIO管脚还是有限,放弃了。使用了0.65寸的小OLED。显示时间:时分秒。当有步行时,显示步行步数和步行时间。无步行时(停止时间超过20秒)显示当前时间。
心得体会:第一次参加活度,作品还是有不少瑕疵。看直播,期待各位大神的作品,到时好好学习一下,尤其是直播中说道用墨水屏做显示,还是怦然心动。很期待。感谢硬禾学堂的活动,感受到了动手的乐趣。