硬件介绍:WeDesign活动是硬禾学堂发起的“一起设计、一起体验”活动。同学们可以通过提交申请来获取芯片、仪器、工具等。如果你能在规定时间内完成相应的任务,就可以获取补贴费用,优质作品更是有丰厚的奖励。
硬禾学堂推出的新活动,第一期是基于灵动微电子的MM32F0140。这颗芯片是使用高性能的 Arm Cortex-M0 为内核的 32 位微控制器,最高工作频率可达 72MHz,内置高速存储器,丰富的增强型 I/O 端口和多种外设。MM32F0140微控制器集成了FlexCAN外设,这是一个在汽车电子系统中常用的外设模块,配合适当的软件,可用于实现连入CAN总线网络的嵌入式系统产品。
看到这颗芯片的介绍,觉着和STM32f103系的芯片超级相似,自己有使用过STM32的芯片,但是最近几年,缺芯问题,已经让STM32系列的芯片变成了理财产品了。正好了解一下国产芯片,于是就写邮件参加了这次的活动。
任务选择:自己定了个小目标:了解一下这个MM32F0140芯片的性能,和用过的STM32F103的芯片做个比较,了解一下差异的地方。想用这个芯片制作一个温度计,使用OLED屏幕做显示。
硬件制作:购买了芯片后,想玩起来,第一步需要有一个开发板。跟着老师学习,了解到这个芯片烧写可以通过SWD方式写程序(这里和stm32f103一致),编写程序使用MDK。手头有颗美信的DS1825,还有几个电子烟上拆下的0.69寸的OLED。正好把这些硬件整合起来。
参考灵动微电子的mini-f0140开发板的电路图,绘制起了自己的电路图。这里结合自己手头的硬件做了些许改动。烧写口,手头有自制的DAPlink,是集成了SWD和串口的DAPLink,这里直接使用排母,就可以和DAPlink模块对插了。电源使用microusb作为电源输入,使用一颗ME6206A33P1G作为降压芯片给MM32F0140供电。OLED显示屏需要高压供电,所以这里增加了OLED的升压电路,提供了一路10v的电源。并且额外使用了2个IO口,用来重置OLED屏幕,和控制OLED的升压使能。IIC口做了上拉。添加了DS1825芯片电路。按键只保留了复位按键。CAN部分电路完全不懂,所以照着官方的电路图抄了过来。所有的IO口,都做了引出。
这里需要说明一下,因为使用了免费的打样,所以PCB并没有直接在KiCad上绘制。而且KiCad中有少量元件封装没有,脑壳痛。这里使用了4层板(板子层数越多,PCB走线越简单^_^)。板子布局考虑了能在面包板上使用,所以两组IO引出的排针,上下布局,间隔为1000mil,不过打完板后发现,插到面包板上,正好和单块面包板宽度一样了,没法直接使用面包板了。这里晶振使用了一颗3脚的8M的晶振,体积更小,而且貌似集成了匹配电容,所以板子上就省去了匹配电容。DS1825放在了右下角,并且开槽和电路板做了物理隔离,防止温度的传导。四个角放置了4个2mm螺丝孔,可以安装铜柱。
收到打样后的板子,使用我的电熨斗大法焊接。其实这次的MM32F0140是32脚,管脚还是比较稀疏的,手工焊接难度不大。不过习惯用电熨斗来焊接了,更快更容易。CAN部分还不会玩,只焊接了芯片,对应的排针暂时没有焊。烧写部分的接口,设计时,用的是90度的排母,最好发现还是用普通排母好看些,DAPLink就垂直着插即可。
软件实现:运气不错,居然焊好板子后,第一次就烧写成功啦!从灵动微电子官网可以下载到MDK的样例程序,下载后打开工程,只需要设置一下烧写方式,选中CMSIS-DAP方式,就可以成功写入程序。这一块感觉比STM32F103做得好,烧写非常稳定。接下来开始实现自己的温度计设计。
下载例程,所有的开始都是从例程开始的。使用mini-f0140_i2c_master_detect_mdk的例程。不得不说灵动微电子代码架构很不错,框架清晰。首先看GPIO的操作,和STM32F103的函数非常相似,操作GPIO口的函数基本都可以按函数名称找到对应的方法。不过遇到第一个问题,例程中没看见延时函数。stm32中延时函数可以使用systick中断来实现,想必这里也一样。查阅资料,果然,先实现systick的延时。在clock_init.c中实现 微妙、毫秒的延时。
void BOARD_InitBootClocks(void)
{
CLOCK_ResetToDefault();
CLOCK_BootToHSE48MHz();
/* UART2. */
RCC_EnableAPB1Periphs(RCC_APB1_PERIPH_UART2, true);
RCC_ResetAPB1Periphs(RCC_APB1_PERIPH_UART2);
/* GPIOA. */
RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOA, true);
RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOA);
/* GPIOB. */
RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOB, true);
RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOB);
/* I2C1. */
RCC_EnableAPB1Periphs(RCC_APB1_PERIPH_I2C1, true);
RCC_ResetAPB1Periphs(RCC_APB1_PERIPH_I2C1);
//systick 初始化
fac_us=CLOCK_SYS_FREQ/8000000;
fac_ms=(uint16_t)fac_us*1000;
}void delay_us(uint32_t nus)
{
uint32_t temp;
SysTick->LOAD=nus*fac_us;
SysTick->VAL=0x00;
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16)));
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;
SysTick->VAL =0X00;
}
void delay_ms(uint16_t nms)
{
uint32_t temp;
SysTick->LOAD=(uint32_t)nms*fac_ms;
SysTick->VAL =0x00;
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16)));
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;
SysTick->VAL =0X00;
}
有了延时,就来实现单总线协议。DS1825使用的是单总线协议,和DS18B20协议一样,只要控制好延时,就能很方便地通过一个GPIO管脚(PB0)写入、读取这枚温度传感器。DS18B20的单总线协议网上可以查到的资料很多,这里就不赘述了。
#include "DS18B20.H"
// 输出模式
void mode_output(void)
{
GPIO_Init_Type gpio_init;
gpio_init.Pins = TEMP_Pin;
gpio_init.PinMode = GPIO_PinMode_Out_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(TEMP_Port, &gpio_init);
}
// 输入模式
void mode_input(void)
{
GPIO_Init_Type gpio_init;
gpio_init.Pins = TEMP_Pin;
gpio_init.PinMode = GPIO_PinMode_In_PullUp;
GPIO_Init(TEMP_Port, &gpio_init);
}
// 复位DS18B20
void DS18B20_Rst(void)
{
mode_output();
GPIO_ClearBits(TEMP_Port, TEMP_Pin); // 拉低
delay_us(750); // 拉低750us
GPIO_SetBits(TEMP_Port, TEMP_Pin); // 输出1
delay_us(15); // 15US
}
// 等待DS18B20的回应
// 返回1:未检测到DS18B20的存在
// 返回0:存在
uint8_t DS18B20_Check(void)
{
uint8_t retry = 0;
mode_input();
while (GPIO_ReadInDataBit(TEMP_Port, TEMP_Pin) && retry < 200)
{
retry++;
delay_us(1);
};
if (retry >= 200)
return 1;
else
retry = 0;
while (!GPIO_ReadInDataBit(TEMP_Port, TEMP_Pin) && retry < 240)
{
retry++;
delay_us(1);
};
if (retry >= 240)
return 1;
return 0;
}
// 初始化DS18B20的IO口 DQ 同时检测DS的存在
// 返回1:不存在
// 返回0:存在
uint8_t DS18B20_Init(void)
{
GPIO_Init_Type gpio_init; // B口时钟已经使能。
gpio_init.Pins = TEMP_Pin; // PORTG.11 推挽输出
gpio_init.PinMode = GPIO_PinMode_Out_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(TEMP_Port, &gpio_init);
GPIO_SetBits(TEMP_Port, TEMP_Pin); // 输出1
DS18B20_Rst();
return DS18B20_Check();
}
// 从DS18B20读取一个位
// 返回值:1/0
uint8_t DS18B20_Read_Bit(void)
{
uint8_t data;
mode_output();
GPIO_ClearBits(TEMP_Port, TEMP_Pin); // 拉低
delay_us(2);
GPIO_SetBits(TEMP_Port, TEMP_Pin); // 拉高
mode_input();
delay_us(5);
if (GPIO_ReadInDataBit(TEMP_Port, TEMP_Pin))
data = 1;
else
data = 0;
delay_us(5);
return data;
}
// 从DS18B20读取一个字节
// 返回值:读到的数据
uint8_t DS18B20_Read_Byte(void)
{
uint8_t i, j, dat = 0;
for (i = 1; i <= 8; i++)
{
j = DS18B20_Read_Bit();
dat = (j << 7) | (dat >> 1);
}
return dat;
}
// 写一个字节到DS18B20
// dat:要写入的字节
void DS18B20_Write_Byte(uint8_t dat)
{
uint8_t j, testb;
mode_output();
for (j = 1; j <= 8; j++)
{
testb = dat & 0x01;
dat = dat >> 1;
if (testb)
{
GPIO_ClearBits(TEMP_Port, TEMP_Pin); // 拉低
delay_us(2);
GPIO_SetBits(TEMP_Port, TEMP_Pin); // 拉高
delay_us(60);
}
else
{
GPIO_ClearBits(TEMP_Port, TEMP_Pin); // 拉低
delay_us(60);
GPIO_SetBits(TEMP_Port, TEMP_Pin); // 拉高
delay_us(2);
}
}
}
// 开始温度转换
void DS18B20_Start(void)
{
DS18B20_Rst();
DS18B20_Check();
DS18B20_Write_Byte(0xcc); // skip rom
DS18B20_Write_Byte(0x44); // convert
}
// 从ds18b20得到温度值
// 精度:0.1C
// 返回值:温度值 (-550~1250)
float DS18B20_Get_Temp(void)
{
uint8_t temp;
uint8_t TL, TH;
int8_t stat;
// short tem;
// float temp
DS18B20_Start(); // ds1820 start convert
DS18B20_Rst();
DS18B20_Check();
DS18B20_Write_Byte(0xcc); // skip rom
DS18B20_Write_Byte(0xbe); // convert
TL = DS18B20_Read_Byte(); // LSB
TH = DS18B20_Read_Byte(); // MSB
if ((TH & 0x80) == 0x80)
{ // 判断温度正负
TH = ~TH;
TL = ~TL + 1; // 负温度处理(DS18B20的负温度是正的反码,即将它取反+1,就得到正的温度)
stat = 1;
}
else
{
stat = 0;
}
if (stat)
return (-1) * ((TH * 256 + TL) * 0.0625);
return (TH * 256 + TL) * 0.0625;
}
显示屏幕使用了一片主控为SSD1307Z的0.69寸96x16的oled屏幕。使用IIC驱动。尝试使用mini-f0140_i2c_master_basic_mdk例程来驱动,能够访问IIC设备。这里有个很有意思的地方,MM32F0140的管脚是可以复用的。我这里IIC使用的是PA4、PA5。所以就需要用AF5指定管脚为I2C的管脚。
void BOARD_InitPins(void)
{
/* PA2 - UART2_TX. */
GPIO_Init_Type gpio_init;
gpio_init.Pins = GPIO_PIN_2;
gpio_init.PinMode = GPIO_PinMode_AF_PushPull; //GPIO_PinMode_AF_PushPull
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_PinAFConf(GPIOA, gpio_init.Pins, GPIO_AF_1);
/* PA3 - UART2_RX. */
gpio_init.Pins = GPIO_PIN_3;
gpio_init.PinMode = GPIO_PinMode_In_Floating; //GPIO_PinMode_In_Floating
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_PinAFConf(GPIOA, gpio_init.Pins, GPIO_AF_1);
/* I2C1_SCL. */
gpio_init.Pins = GPIO_PIN_5;
gpio_init.PinMode = GPIO_PinMode_AF_OpenDrain;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio_init);
GPIO_PinAFConf(GPIOA, gpio_init.Pins, GPIO_AF_5);
/* I2C1_SDA. */
gpio_init.Pins = GPIO_PIN_4;
gpio_init.PinMode = GPIO_PinMode_AF_OpenDrain;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_PinAFConf(GPIOB, gpio_init.Pins, GPIO_AF_5);
}
可惜折腾了很久,可以成功读到OLED的IIC地址,但是就没法显示内容,不太清楚什么原因。最后直接移植了stm32f103的模拟IIC的代码,成功驱动了OLED。
// GPIO初始化
void SI2C_GPIOInitConfig(void)
{
GPIO_Init_Type gpio_init;
/* I2C1_SCL. */
gpio_init.Pins = I2C_SCL_PIN | I2C_SDA_PIN;
gpio_init.PinMode = GPIO_PinMode_Out_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_PinAFConf(I2C_Port, gpio_init.Pins, GPIO_AF_1);
GPIO_SetBits(I2C_Port, I2C_SCL_PIN | I2C_SDA_PIN); // 设置初始电平为高电平
}
// SDA设置为输出模式
static void SDA_OUT(void)
{
GPIO_Init_Type gpio_init;
gpio_init.Pins = I2C_SDA_PIN;
gpio_init.PinMode = GPIO_PinMode_Out_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C_Port, &gpio_init);
GPIO_SetBits(I2C_Port, I2C_SDA_PIN);
}
// SDA输入模式
static void SDA_IN(void)
{
GPIO_Init_Type gpio_init;
gpio_init.Pins = I2C_SDA_PIN;
gpio_init.PinMode = GPIO_PinMode_In_PullUp;
GPIO_Init(I2C_Port, &gpio_init);
}
void SI2C_Start()
{
SDA_OUT(); // 设置SDA输出模式
SI2C_SDA_1(); // 将SDA设置为高电平
SI2C_SCL_1(); // 将SCL设置为高电平
delay_us(8);
SI2C_SDA_0(); // 将SDA设置为低电平
delay_us(8);
SI2C_SCL_0(); // 将SCL设置为低电平
}
void SI2C_Stop()
{
SDA_OUT(); // 设置SDA输出模式
SI2C_SDA_0(); // 将SDA设置为低电平
SI2C_SCL_1(); // 将SCL设置为高电平
delay_us(8);
SI2C_SDA_1(); // 将SDA设置为高电平
delay_us(8);
}
void SI2C_SendACK(uint8_t ack)
{
SDA_OUT();
SI2C_SCL_0();
delay_us(8);
if (ack)
SI2C_SDA_1();
else
SI2C_SDA_0();
SI2C_SCL_1();
delay_us(8);
SI2C_SCL_0();
delay_us(8);
}
uint8_t SI2C_RecvACK(void)
{
uint8_t ucErrTime = 0;
SDA_IN(); // 设置SDA输入模式
// I2C_SDA_1();
// delay_us(4);
SI2C_SCL_1(); // 将SCL设置为高电平
delay_us(4);
while (SI2C_SDA_READ()) // 检测SDA上是否出现低电平(ACK)
{
ucErrTime++;
if (ucErrTime > 250) // 超时间差
{
SI2C_Stop(); // 停止通信
return 1;
}
}
SI2C_SCL_0(); // 将SCL设置为低电平
return 0;
}
void SI2C_SendByte(uint8_t dat)
{
uint8_t t;
SDA_OUT();
SI2C_SCL_0();
for (t = 0; t < 8; t++)
{
// SDA=(dat&0x80)>>7;
if (dat & 0x80)
SI2C_SDA_1(); // 将数据线上的电位设置好
else
SI2C_SDA_0();
dat <<= 1;
delay_us(5);
SI2C_SCL_1(); // 时钟变化,数据发送出去
delay_us(5);
SI2C_SCL_0();
delay_us(5);
}
SI2C_RecvACK();
}
uint8_t SI2C_RecvByte(void)
{
int i = 0;
uint8_t byte = 0;
SDA_IN();
for (i = 0; i < 8; i++)
{
delay_us(5);
SI2C_SCL_1(); // 先接受一次数据线上的数据到寄存器
delay_us(5);
byte <<= 1;
if (SI2C_SDA_READ()) // 判断寄存器中的数据
{
byte |= 0x01;
}
SI2C_SCL_0();
delay_us(5);
}
return byte;
}
/****************************************************************************
* 函 数 名: i2c_CheckDevice
* 功能说明: 检测I2C总线设备,CPU向发送设备地址,然后读取设备应答来判断该设备是否存在
* 形 参:_Address:设备的I2C总线地址
* 返 回 值: 返回值 0 表示正确, 返回1表示未探测到
****************************************************************************/
uint8_t I2C_CheckDevice(uint8_t _Address)
{
uint8_t ucAck;
SI2C_Start(); /* 发送启动信号 */
/* 发送设备地址+读写控制bit(0 = w, 1 = r) bit7 先传 */
SI2C_SendByte(_Address | I2C_WR);
ucAck = SI2C_RecvACK(); /* 检测设备的ACK应答 */
SI2C_Stop(); /* 发送停止信号 */
return ucAck;
}
//========OLED控制部分=====================================================
void OledWriteCmd(uint8_t cmd)
{
SI2C_Start();
SI2C_SendByte(0x78);
SI2C_SendByte(0x00);
SI2C_SendByte(cmd);
SI2C_Stop();
}
void OledWriteData(uint8_t dt)
{
SI2C_Start();
SI2C_SendByte(0x78);
SI2C_SendByte(0x40);
SI2C_SendByte(dt);
SI2C_Stop();
}
void OledFillData(uint8_t Data)
{
uint8_t i, j;
for (i = 0xB0; i < 0xB3; i++)
{
OledWriteCmd(i);
OledWriteCmd(0x00);
OledWriteCmd(0x12);
for (j = 0; j < 128; j++)
{
OledWriteData(Data);
}
}
}
void oled_init(void)
{
SI2C_GPIOInitConfig();
OledWriteCmd(0xAE); // display off
OledWriteCmd(0xd5); // Set Display ClocDivide Ratio/Oscillator Frequency
OledWriteCmd(0xc4); // 100HZ
OledWriteCmd(0xa8); //--set multiplex ratio(1 to 64)
OledWriteCmd(0x0f); // set 16mux
OledWriteCmd(0xd9); // Set Pre-charge Period
OledWriteCmd(0x22);
OledWriteCmd(0x20); // Set Memory Addressing Mode
OledWriteCmd(0x02);
if (DISPDIC == 0)
{
OledWriteCmd(0xa0); // seg re-map 0->127
OledWriteCmd(0xc8); // COM scan direction COM(N-1)-->COM0
}
else
{
OledWriteCmd(0xa1); // seg re-map 0->127
OledWriteCmd(0xc0); // COM scan direction COM(N-1)-->COM0
}
OledWriteCmd(0xda); // Set COM Pins Hardware Configuration
OledWriteCmd(0x12); //
OledWriteCmd(0x81); // Set Contrast Control
OledWriteCmd(0x0c); //
OledWriteCmd(0xb0); // Set Page Start Address for Page Addressing Mode
OledWriteCmd(0xd3); // Set Display offset
OledWriteCmd(0x1f); //
OledWriteCmd(0xa6); // Display Normal(ÕýÏÔ)
OledWriteCmd(0xa4); // Entire Display Off
OledWriteCmd(0xdb); // Set VCOMH Level()
OledWriteCmd(0x30); // 0.83*VCC
OledWriteCmd(0xaf); // 打开显示
OledFillData(0x00); // 初始清屏
}
//=========================================================//OLED关闭
void OledOff(void)
{
OledWriteCmd(0xAE); // 关闭显示
}
//=========================================================//OLED打开
void OledOn(void)
{
OledWriteCmd(0xAF); // 打开显示
}
//=========================================================//设置Y地址
void OledSetAddY(uint8_t AddY)
{
OledWriteCmd(0xB0 + AddY); // OLED地址Y
}
//=========================================================//设置X地址
void OledSetAddX(uint8_t AddX)
{
AddX *= 6; // 每个字符总列数
OledWriteCmd(AddX % 16); // 低字节(0-15)
if (DISPDIC == 0)
{
OledWriteCmd(0x12 + (AddX / 16)); // 高字节()0x10为偏移量
}
else
{
OledWriteCmd(0x10 + (AddX / 16));
}
}
//=========================================================//在指定位置写数据
// 每个字占2位,16位总共可以写8个数字
void OledWriteWord(uint8_t AddX, uint8_t ch)
{
uint8_t x;
OledSetAddX(AddX);
OledSetAddY(0);
for (x = 0; x < 8; x++)
{
OledWriteData(MCharTab[ch][x]);
}
OledSetAddX(AddX);
OledSetAddY(1);
for (x = 0; x < 8; x++)
{
OledWriteData(MCharTab[ch][x + 8]);
}
}
//=========================================================//在指定位置写数据
void OledWriteChar(uint8_t AddX, uint8_t AddY, uint8_t ch)
{
unsigned char x;
OledSetAddX(AddX);
OledSetAddY(AddY);
for (x = 0; x < 7; x++)
{
OledWriteData(CharTab[ch][x]);
}
}
// 根据温度显示 cold cool warm hot
void OledDispStr(int temperature)
{
if (temperature >= 13 && temperature < 20)
{ // cool
OledWriteChar(12, 1, 12);
OledWriteChar(13, 1, 24);
OledWriteChar(14, 1, 24);
OledWriteChar(15, 1, 21);
}
if (temperature < 13)
{ // cold
OledWriteChar(12, 1, 12);
OledWriteChar(13, 1, 24);
OledWriteChar(14, 1, 21);
OledWriteChar(15, 1, 13);
}
if (temperature >= 20 && temperature < 29)
{ // warm
OledWriteChar(12, 0, 32);
OledWriteChar(13, 0, 10);
OledWriteChar(14, 0, 27);
OledWriteChar(15, 0, 22);
}
if (temperature >= 29)
{ // hot
OledWriteChar(12, 0, 17);
OledWriteChar(13, 0, 24);
OledWriteChar(14, 0, 29);
OledWriteChar(15, 0, 36);
}
}
// 显示浮点数,整数位显示3位,小数位显示1位 共5个数字
void OledDispTemperature(float num)
{
uint8_t i;
OledWriteWord(0, 10);
if (num < 0)
{
num = -num;
OledWriteWord(0, 14);
}
else
{
i = num / 100;
if (i > 0)
{
OledWriteWord(0, i);
}
num = num - i * 100;
}
i = num / 10;
if (i > 0)
{
OledWriteWord(2, i);
}
num = num - i * 10;
i = num;
if (i >= 0)
{
OledWriteWord(4, i); // 个位
}
num = num - i;
OledWriteWord(6, 11); // 显示小数点
i = (num * 100 + 5) / 10;
if (i >= 0)
{
OledWriteWord(8, i);
}
OledWriteChar(10, 1, 41);
}
心得体会:感谢硬禾学堂提供的这次活动。通过接触了MM32F0140的这几周时间,真心觉得做得比STM32F103好。烧写、调试都很稳定。更多的想了解CAN总线的使用,但是自己没有接触过,借助这次机会获得硬件,向老师和同学们学习一番。