一、项目介绍
这是WeDesign第1期项目:基于灵动微电子MM32F0140微控制器的设计体验
1、使用KiCad软件设计一款基于MM32F0140微控制器的嵌入式系统应用。
- 小型的GUI动画界面:能够实时显示数据状态的仪表盘
- 通信接口桥:对接一些使用复杂接口或者定制协议的功能模块,通过基于UART串口 的AT命令实现控制
- ECU电控单元:基于CAN总线,实现大灯控制、雨刮器、车窗、方向盘等控制系统 的原型
- 小型机器人:四轴飞行器、六足机器人、智能小车底盘等部分功能
- ……
2、我的设计介绍
我设计的嵌入式系统应用是小型高精度温度采集仪,主要特点和功能:
- 分辨率:0.001℃
- 采集环境温度
- 绘制近2分钟温度时域曲线
- 可调节采集周期
- 串口上传温度数据
3、MM32F0140微控制器介绍
本期芯片具体型号:MM32F0144C4P
MM32F0140是使用高性能的 Arm® Cortex-M0 为内核的 32 位微控制器,最高工作频率可达 72MHz,内置高速存储器,丰富的增强型 I/O 端口和多种外设。MM32F0140微控制器集成了FlexCAN外设,这是一个在汽车电子系统中常用的外设模块,配合适当的软件,可用于实现连入CAN总线网络的嵌入式系统产品。
片上资源:
- 64KB Flash,8KB SRAM
- 1 个 12 位的 ADC和 1 个比较器
- 1 个 16 位通用定时器、1 个 32 位通用定时器、3 个 16 位基本定时器、1 个 16 位高级定时器
- 3 个 UART 接口、2 个 SPI 接口和 1 个 I2C 接口
- 1 个 FlexCAN 接口
- 工作电压为 0V - 5.5V
- 高可靠性: 支持最高 ±6000V HBM ESD,±2000V CDM ESD,高温 Latch-up 可耐电流 ±300mA
- 工作温度范围(环境温度)-40℃ - 85℃ 工业型和-40℃ - 105℃ 扩展工业型(后缀为V)
- 多种省电工作模式支持低功耗应用的需求
- 与 MM32F031 引脚兼容
- 提供 LQFP48、LQFP32、QFN32 和 TSSOP20 封装
适合于多种应用场合:
- 手机充电头
- 快充控制
- 电池管理
- 电梯外呼板
- 电动工具
- 消防监控
- 灯光控制
- 断路器
- 汽车诊断仪
- 后装汽车协控制器
4、KiCad软件介绍
KiCad是一种免费、开源的EDA设计工具,它能够创建电路原理图并进行PCB布局布线,它具有一个集成化的开发环境,在其之下KiCad包含了如下几款非常精致、相互独立的软件工具。
KiCad对板子的大小没有限制,很容易支持到32铜层的电路板,到最多14层技术层和最多4层附加层。它能够产生用于生产制造PCB板的所有必要文件:用于光绘的Gerber文件、钻孔文件、元器件定位文件等等。
KiCad是一个跨平台的程序,用具有wxWidgets的C++编写,可在FreeBSD,Linux,Microsoft Windows和Mac OS X上运行。许多元器件库都可用,用户可以添加自定义元器件,添加的时候可以按你正在设计的项目安装,也可以安装成“全局使用”,也就是任何项目设计的时候都可以直接调用。 还有一些工具可以帮助从其它EDA应用程序导入元器件,例如EAGLE,但貌似还不支持直接导入Altium Designer、OrCAD等工具的设计文件。 配置文件有明确记录的纯文本,这有助于连接到版本控制系统以及自动元器件生成脚本。
KiCad支持多种语言,包括中文。
二、项目设计思路(含设计框图)
1、硬件结构:
硬件以MM32F0144C4P为核心相连各模块构成,具体可以查看附件原理图。
2、软件流程框图:
软件流程相对简单,没有使用中断和实时操作系统。接下来介绍具体实现。
3、基本功能实现:
- 调试串口使用串口一PA9 PA10引脚,选择115200波特率;
- 板上有一颗可以程序控制的指示灯;
- LCD选择0.96寸ISP,驱动芯片为ST7735S;
- ADC芯片选择美国微芯的24位MCP3561,温度采集使用下图方案;
- 电池充电使用TP4054芯片,最大500ma充电电流。
- 电池电量监测使用单片机ADC测量电阻分压进行判断
一般手机设计待机电量时, 比如有4格5档(4-3-2-1-0)的电量指示.我参考一下资料也同样设计了5档电量显示
4.20V~3.90V满格3.90V~3.80V三格
3.80V~3.72V两格
3.72V~3.65V一格
3.65以下,低电压告警。
- 外扩FLASH使用了W25Q128,16MB容量;
- 板载TTL转USB使用CH340;
- CAN 通信使用TJA1051T;
- 数字供电使用LP2981-3.3 160uV噪声
- 模拟供电使用ADP7104ACP-3.3 15uV噪声
- 参考电压使用ADR441ARMZ 1.2uV噪声
- 程序下载端口通过1.27mm排针引出
- 所有电容电阻使用0402封装
- 图片通过取模软件实现
- 温度采集周期可以通过按键调整
- 模块化代码编程,均分类放于hardware文件夹中
- 数据使用卡尔曼进滤波算法 效果如下
4、主函数代码:
#include "board_init.h"
#include "sfud.h"
#include "lfs.h"
#include "led.h"
#include "usart.h"
#include "hal_uart.h"
#include "lcd_init.h"
#include "lcd.h"
#include "pic.h"
#include "MCP3564_DRV.h"
#include "Kalman.h"
#include "adc.h"
#include "graph.h"
#include "CQueue.h"
#include "key.h"
float MedianFilter(float *pData,unsigned char nSize);
float temper[12]={0};
float alldata=0;
float temp=0,temp_f=0,battery_v;
uint8_t i=0;
uint32_t adc,value,battery_adc;
uint32_t t=0,tt=0;
uint32_t wave0[GRAPH_WIDTH], wave1[GRAPH_WIDTH], *cur_wave, *prev_wave, curr_wave_length = 0, prev_wave_length = 0;
uint32_t w=0; //显示波形控制
uint32_t num=12;//采集频率控制
uint32_t key=0;//按键监测
Queue i_temperature;
int main(void)
{
BOARD_Init();//时钟和引脚
leds_init();
uart_init(115200);
app_adc_init();
LCD_Init();//LCD初始化
LCD_Fill(0,0,LCD_W,LCD_H,BLACK);
LCD_ShowChinese32x32(128,51,"℃",RED,BLACK,32,0);
display_coordinate_1();//显示坐标轴
MCP3564_DRV_Init(0,&MCP3563Confg);
MCP3564_DRV_Init(0,&MCP3563Confg);//初始化两次,只
initQueue(&i_temperature);
Kalman_Init();
while (1)
{
t++;
while (GPIO_ReadInDataBit(GPIOB,GPIO_PIN_3)==1)
{
if(t%10==0)
{
LCD_Fill(7,78,28,80,BLACK);
LCD_ShowPicture(7,28,18,50,gImage_6);
}else if(t%5==0)
{
LCD_Fill(7,28,25,30,BLACK);
LCD_ShowPicture(7,30,18,50,gImage_6);
}
battery_adc=app_adc_run_conv();
battery_v=(float)battery_adc*6.6/4095;
if(battery_v>=3.9)//电池电量
{
LCD_ShowPicture(3,3,25,11,gImage_5);
}else if(battery_v<3.9&&battery_v>=3.8)
{
LCD_ShowPicture(3,3,25,11,gImage_4);
}else if(battery_v<3.8&&battery_v>=3.72)
{
LCD_ShowPicture(3,3,25,11,gImage_3);
}else if(battery_v<3.72&&battery_v>=3.65)
{
LCD_ShowPicture(3,3,25,11,gImage_2);
}else if(battery_v<3.65)
{
LCD_ShowPicture(3,3,25,11,gImage_1);
}
}//等待就绪
leds_on();
MCP3564_DRV_ReadReg24(0,MCP3563_DeviceAddressList[0],MCP3564_ADCDATA_ADDRESS,&adc);//读取adc
temp=(float)adc*6150/4/8388607;//电阻
temp=(temp-1000)/3.85;
if(tt==0)//刚上电显示
{
kfp_c.out = temp;
LCD_ShowFloatNum1(30,51,temp,5,RED,BLACK,32);
tt=1;
}
temp_f=KalmanFilter(&kfp_c,temp);
temper[i]=temp_f;
i++;
key=KEY_Scan(0);
if(key==KEYA_PRES)
{
i=0;
if(num<12)
{
num++;
printf("frequency step down\r\n");
}
}
else if (key==KEYB_PRES)
{
i=0;
if(num>1)
{
num--;
printf("Frequency increase\r\n");
}
}
if(i==num)
{
i=0;
cur_wave = (w % 2) ? wave1 : wave0;
prev_wave = (w % 2) ? wave0 : wave1;
prev_wave_length = curr_wave_length;
alldata=MedianFilter(temper,num);
enQueue(&i_temperature,(uint32_t)(alldata*1000));//添加数据
compute_yaxis_length(&i_temperature);
curr_wave_length = generate_wave(&i_temperature, cur_wave);
display_wave(cur_wave, prev_wave, curr_wave_length, prev_wave_length);
w = (w + 1) % GRAPH_WIDTH;
LCD_ShowFloatNum1(30,51,alldata,5,RED,BLACK,32);
LCD_ShowFloatNum(2,15,((float)i_temp_max-(float)i_temp_min)/1000.0,3,RED,BLACK,12);
printf("%.3f,%.3f\r\n",battery_v,alldata);
}
leds_off();
}
}
4、波形显示
该功能应该是本项目的难点,我参考氢化脱氯次氯酸https://www.eetree.cn/project/detail/1715该项目实现办法,进行微调,新加实现Y轴根据数据实时调整的功能
该段为该同学的实现思路:开发板可以保存近120秒的温度信息,并绘制在LCD屏幕上,从右至左滚动刷新,即最右侧的温度为当前温度,最左侧的温度为120秒前的温度。由于每次温度的更新需要在末尾添加最新的温度值,并删除底部最早的温度值,这种先入先出的特性可以由队列实现,且由于长度固定(120个温度值),所以绘制曲线所需要的温度信息被储存在由静态数组实现的循环队列中。队列的长度为120,需要一个额外元素来判断循环队列是否已满。此外绘制曲线时需要查询所有时刻的温度值,针对该需求额外实现了一个队列索引的方法。
请大家直接进入该同学页面学习,使用队列的方式实现曲线绘制,再次感谢氢化脱氯次氯酸
队列相关代码
#include "CQueue.h"
void initQueue(Queue *q)
{
q->front = 0;
q->rear = 0;
}
int isQueueEmpty(Queue *q)
{
return q->front == q->rear;
}
int isQueueFull(Queue *q)
{
return (q->rear + 1) % MAX_QUEUE_SIZE == q->front;
}
void enQueue(Queue *q, int32_t value)
{
if (isQueueFull(q)) {
deQueue(q);
}
q->data[q->rear] = value;
q->rear = (q->rear + 1) % MAX_QUEUE_SIZE;
}
int deQueue(Queue *q)
{
if (isQueueEmpty(q)) {
return 1;
} else {
q->front = (q->front + 1) % MAX_QUEUE_SIZE;
return 0;
}
}
int getQueueElem(Queue *q, uint32_t index, int32_t *value)
{
uint32_t position;
if (index >= MAX_QUEUE_SIZE - 1 || index < 0)
return -1;
if (index >= (q->rear + MAX_QUEUE_SIZE - q->front) % MAX_QUEUE_SIZE)
return 1;
position = (q->front + index) % MAX_QUEUE_SIZE;
*value = q->data[position];
return 0;
}
曲线显示相关代码
#include "lcd.h"
#include "graph.h"
void display_coordinate_1(void)
{
LCD_DrawLine(29,0,29,50,BLUE);//坐标轴
LCD_DrawLine(29,50,160,50,BLUE);
LCD_DrawPoint(29,25,GREEN);
LCD_DrawPoint(30,25,GREEN);
LCD_DrawPoint(56,50,GREEN);
LCD_DrawPoint(82,50,GREEN);
LCD_DrawPoint(108,50,GREEN);
LCD_DrawPoint(134,50,GREEN);
LCD_DrawPoint(160,50,GREEN);
LCD_DrawPoint(56,49,GREEN);
LCD_DrawPoint(82,49,GREEN);
LCD_DrawPoint(108,49,GREEN);
LCD_DrawPoint(134,49,GREEN);
LCD_DrawPoint(160,49,GREEN);
}
void display_coordinate(void)
{
uint32_t x, y;
LCD_DrawRectangle(GRAPH_START_X - 1, GRAPH_START_Y - 1, GRAPH_START_X + GRAPH_WIDTH + 1, GRAPH_START_Y + GRAPH_HEIGHT + 1, AXIS_COLOR);
for (x = 1; x < GRAPH_WIDTH / X_SCALE; x++)
{
LCD_DrawPoint(X_SCALE * x + GRAPH_START_X, GRAPH_START_Y + GRAPH_HEIGHT, AXIS_COLOR);
LCD_DrawPoint(X_SCALE * x + GRAPH_START_X, GRAPH_START_Y + GRAPH_HEIGHT - 1, AXIS_COLOR);
LCD_DrawPoint(X_SCALE * x + GRAPH_START_X, GRAPH_START_Y, AXIS_COLOR);
LCD_DrawPoint(X_SCALE * x + GRAPH_START_X, GRAPH_START_Y + 1, AXIS_COLOR);
}
for (y = 1; y < GRAPH_HEIGHT / Y_SCALE; y++)
{
LCD_DrawPoint(GRAPH_START_X, Y_SCALE * y + GRAPH_START_Y, AXIS_COLOR);
LCD_DrawPoint(GRAPH_START_X + 1, Y_SCALE * y + GRAPH_START_Y, AXIS_COLOR);
LCD_DrawPoint(GRAPH_START_X + GRAPH_WIDTH, Y_SCALE * y + GRAPH_START_Y, AXIS_COLOR);
LCD_DrawPoint(GRAPH_START_X + GRAPH_WIDTH - 1, Y_SCALE * y + GRAPH_START_Y, AXIS_COLOR);
}
}
void display_axis(void)
{
uint32_t x, y;
LCD_ShowChar(GRAPH_START_X + 12, GRAPH_START_Y + GRAPH_HEIGHT, '-', AXIS_COLOR, BACKGROUND_COLOR, 12, 0);
LCD_ShowIntNum(X_SCALE * 1 + GRAPH_START_X - 3, GRAPH_START_Y + GRAPH_HEIGHT, 80, 2, AXIS_COLOR, BACKGROUND_COLOR, 12);
LCD_ShowChar(GRAPH_START_X + 52, GRAPH_START_Y + GRAPH_HEIGHT, '-', AXIS_COLOR, BACKGROUND_COLOR, 12, 0);
LCD_ShowIntNum(X_SCALE * 3 + GRAPH_START_X - 3, GRAPH_START_Y + GRAPH_HEIGHT, 40, 2, AXIS_COLOR, BACKGROUND_COLOR, 12);
LCD_ShowIntNum(GRAPH_START_X + 97, GRAPH_START_Y + GRAPH_HEIGHT, 0, 1, AXIS_COLOR, BACKGROUND_COLOR, 12);
for (y = 0; y <= GRAPH_HEIGHT / Y_SCALE; y++)
{
LCD_ShowIntNum(0, Y_SCALE * y + GRAPH_START_Y - 5, 60-y*10, 2, AXIS_COLOR, BACKGROUND_COLOR, 12);
}
}
void display_wave(const uint32_t *y, const uint32_t *y_prev, uint32_t y_wave_length, uint32_t y_prev_wave_length)
{
uint32_t x;
uint32_t start_point = GRAPH_START_X + GRAPH_WIDTH - y_prev_wave_length;
for (x = start_point; x < GRAPH_START_X + GRAPH_WIDTH - 1; x++)
{
LCD_DrawLine(x, y_prev[x-start_point], x + 1, y_prev[x-start_point+1], BACKGROUND_COLOR);
}
LCD_DrawPoint(x, y_prev[x-start_point], BACKGROUND_COLOR);
start_point = GRAPH_START_X + GRAPH_WIDTH - y_wave_length;
for (x = start_point; x < GRAPH_START_X + GRAPH_WIDTH - 1; x++)
{
LCD_DrawLine(x, y[x-start_point], x + 1, y[x-start_point+1], WAVE_COLOR);
}
LCD_DrawPoint(x, y[x-start_point], WAVE_COLOR);
}
int32_t i_temp_max = 100000;//原来温度数据×1000
int32_t i_temp_min = 0;
void compute_yaxis_length(Queue *i_temperature)
{
uint32_t i=0;
uint32_t max,min;
uint32_t sum=0;
uint32_t length=0;
length=(i_temperature->rear + MAX_QUEUE_SIZE - i_temperature->front) % MAX_QUEUE_SIZE;
max = i_temperature->data[i];
min = max;
for(int i=0;i<length;i++)
{
sum += i_temperature->data[i];
if(i_temperature->data[i]>max)
{
max = i_temperature->data[i];
}
if(i_temperature->data[i]<min)
{
min = i_temperature->data[i];
}
}
i_temp_max=max;
i_temp_min=min;
}
uint32_t generate_wave(Queue *i_temperature, uint32_t y[GRAPH_WIDTH])
{
uint32_t i;
int32_t temp;
for (i = 0; i < GRAPH_WIDTH; i++)
{
if (getQueueElem(i_temperature, i, &temp))
break;
if (temp <= i_temp_max && temp >= i_temp_min)
y[i] = (GRAPH_HEIGHT - 1) * (i_temp_max - temp) / (i_temp_max - i_temp_min) + GRAPH_START_Y;
else if (temp > i_temp_max)
y[i] = GRAPH_START_Y;
else if (temp < i_temp_min)
y[i] = GRAPH_HEIGHT + GRAPH_START_Y - 1;
}
return i;
}
三、画原理图、PCB制板、调试过程过程中遇到的问题及解决方法
1、CAN芯片的电源供电没有连接
2、三极管S8050由于导入直插器件原理图去匹配贴片封装,导致发射极和基极对调,后续反过来焊接S8050解决
3、调试ISP 0.96寸屏幕时,使用硬件SPI,由于发送数据后没有读取接收寄存器,导致屏幕无法点亮。
4、工程文件路径中不能包含中文字符,否则无法跳转函数定义
四、实现结果
PCB渲染图展示:
PCB2D图片展示:
实物半成品照片展示:
实物成品照片展示:
GUI展示:
五、MM32F0140芯片的优势与局限
1、优势
- 使用MindSDK开发很方便,可参考例程丰富。
- 库函数常用函数都具备,开发简单方便。
- 开发工具可以使用keil,上手容易。
- 硬件外围电路简单。
2、局限
- 目前使用的这款单片机RAM只有8K,跑一些现成的GUI库可能比较困难
- 没有图形化界面配置外设,需要花费开发人员时间去手动配置。
六、未来规划和建议
该项目已经成功实现温度采集并绘制曲线功能,还有许多可以提升与扩展的地方:
- 将温度数据保存到板上flash芯片中,需要的时候可以上传上位机查看分析。
- 通过板上CAN总线进行多节点温度监测并汇总到一个节点查看分析
- 尝试移植现成GUI库,实现更炫酷的页面。
- 采用一些算法优化功耗,比如降低主频、空闲时关闭屏幕等。
七、心得体会
熟悉其他PCB设计工具的开发者,可以在较短时间上手KICAD,KICAD拥有丰富的库资源,操作相对简单,运行要求配置不高,开源免费,还有不少插件可以使用,比如交互式BOM插件,非常方便查看物料信息和调试,我认为是一款不错的工具,但是有些高级的功能暂时还不具备,比如自动扇孔等。
MM32F0140单片机它的flash比较大,有64K,但是RAM相对较小,有8K。这个配置我觉得可以满足多数小型产品的需求;它和ST公司的产品比较接近,熟悉STM32的开发者可以立即上手开发;我在使用过程中也没有出现异常情况,个人还是很满意的,一次非常不错的体验。
对与铂电阻采集电路,我使用的应该是业界常用方案,但是由于这次选择的ADC芯片噪声较大无法做到0.0001℃的分辨率,那么选用的低噪声的供电方案和参考电压方案也就没有发挥出应有的性能,这一点比较遗憾。给我的启示就是以后选择芯片要多注意噪声性能,提前计算以及仿真判断是否能满足自己的需求。目前正在学习低噪声高精度的一些采集案例,有做相关方向的同学可以交个朋友交流交流。另外本项目还有需要地方可以改进,有兴趣的小伙伴可以在我的基础上进行改进。