资讯详情

基于ARM-Cortex M3/4的GNU汇编的嵌入式程序设计之二——实现硬件I2C访问MS4525压力传感器

基于ARM-Cortex M3的GNU嵌入式程序设计汇编2-实现硬件I2C访问MS4525压传感器

  • 一、目标
    • 需求描述
    • 有关的问题
  • 二、代码及解释
  • 三、运行情况
  • 四、总结

一、目标

根据前面对汇编的介绍,我们来实现一个stm32f103的硬件I2C从MS4525压力传感器读力传感器。作者的硬件是103I2C2连到了MS4525上面。

需求描述

需求 描述
C函数封装 将函数封装成C语言可调用的函数,并在其他C函数中调用
读取传感器数值 函数执行一次,从传感器读取压力值。也就是说,每次用传感器读两个字节

有关的问题

很多人说stm32f103的硬件i2c有bug,其实主要有两个

Bug 解决方案
初始化时I2C的GPIO时钟使能无效 假如你用了cube引脚的配置,即使用hal库初始化HAL_I2C_MspInit()函数优化。参见作者的另一篇文章:《STM32F103与4525I压力传感器通信中的硬件I2C解决方案
SR2_busy位定位1 烧录一个不用I2C端口程序,关机重启。调试前修改代码。只有硬件关闭才能解锁。对调试不友好。

还有人说,总是不可调。事实上,这很可能是因为C语言不能严格执行该端口要求的寄存器读取顺序。 首先是使能CR1的PE,发送START。

发送START 置位CR1 - START位后,轮询SR - SB位。如果置位了,则要读一次SR1.写入从机地址(即)MS4525地址),才能清理SB位。

轮询SR1- ADDR位置,如果位置,需要读一次SR1再读一次SR2清除。之后,因为我们只读两个字节,所以我们必须参考手册后面的文本。

所以,如果你读手册,一定要小心。

至于MS4525侧,其实比较简单,就是发送起始符,发送地址,读两个字节,发送NACK和终止符。即使是完成会话。

二、代码及解释

了解情况后,我们开始写程序。 注意,很多时候,我们可能有一个系统presSen.c不能直接创建的文件presSen.s,因为这两个文件一旦编译,就会生成presSen.o,会冲突。 作者输出了一个叫做的函数`uint16_t asm_Func_presSen_getVal(void);

从开头的四句话startup_stm32f103xe.s复制。解锁32位。Thumb-2指令。

考虑到将在程序中使用I2C2相关寄存器CR1、DR、SR1、SR2, 使用的位置主要是CR1的START、POS、ACK、STOP和PE,SR1的SB、RXNE、ADDR、BTF。因为这些都是local因此,不需要提前使用符号,可以直接使用.local声明。我们给I2C2_BaseAddr赋值是该端口寄存器的起始地址,其余相关寄存器只需定义偏移量即可。如果您不记得这些寄存器的偏移,请访问手册中的这个位置。也可以从手册或C头文件中查看基地址。这样用LDR和STR可通过基地址 直接访问偏移量。

那些.section .text.asm_Func_presSen_getVal以下设置选项,参考一些关于汇编的伪指令

我们存储了多少数字。就像用C语言看几个变量一样。如果不到8个,只需要一个寄存器。本文只有一个函数,来回操作三个外部寄存器,加上一个寄存器记录基地址,四个寄存器开始,不要打开内存,写不够再去BSS开段就是。

/* * presSen_asm.s * * Created on: 2022年6月17日 * Author: SystemUser */  .syntax unified .cpu cortex-m3 .fpu softvfp .thumb  .global asm_Func_presSen_getVal  .set  I2C2_BaseAddr,   0x40005800 .set  CR1,    0x00 .set  DR,     0x10 .set  SR1,    0x14 .set  SR2,    0x18  .set  I2C_CR1_START,  0x100 .set  I2C_CR1_STOP,  0x200 .set  I2C_CR1_ACK,  0x400 .set I2C_CR1_POS,  0x800 .set I2C_CR1_PE,   0x01
.set	I2C_CR1_POS_ACK,	I2C_CR1_POS | I2C_CR1_ACK
.set 	I2C_SR1_ADDR, 		0x02
.set 	I2C_SR1_SB, 		0x01
.set 	I2C_SR1_BTF, 		0x04

.set 	sensor_addr,		0x51

	.section .text.asm_Func_presSen_getVal/*,"ax",%progbits*/
	.type asm_Func_presSen_getVal,%function
/* * Accroding to the manual, Case of two bytes to be received: * – Set POS and ACK * – Wait for the ADDR flag to be set * – Clear ADDR * – Clear ACK * – Wait for BTF to be set * – Program STOP * – Read DR twice */

/*r3 is used to hold CR1, r4 for I2C2_BaseAddr, r5 for SR1, R6 for SR2*/
asm_Func_presSen_getVal:
	push	{ 
        r4-r7, lr}
/*for test*/
/*test over*/
	ldr	 	r4, =I2C2_BaseAddr; /* load the address of the I2C2 base register*/
	ldr  	r3, [r4, #CR1];

Enable_the_I2C2:
	orr 	r3, #I2C_CR1_PE	/*Set the PE*/
	str		r3, [r4, CR1];
Send_a_Start:
	orr		r3, #I2C_CR1_START  /*Send a start*/
	str		r3, [r4, CR1];

Check_if_the_i2c_start_isSent:
	ldr 	r5, [r4, #SR1];	/*Keep reading the sr1 register*/
	and		r5, #I2C_SR1_SB
	cmp		r5, #I2C_SR1_SB
	bne		Check_if_the_i2c_start_isSent;
Send_the_Addr:
	ldr 	r5, [r4, #SR1];
	mov		r3, #sensor_addr;
	str		r3, [r4, DR];

Set_POS_and_ACK:
	ldr  	r3, [r4, #CR1];
	orr 	r3, #I2C_CR1_POS_ACK	/*Set the ACK and POS*/
	str		r3, [r4, CR1];

Wait_for_the_ADDR_flag_to_be_set:
	ldr  	r5, [r4, #SR1];
	and		r5, #I2C_SR1_ADDR
	cmp		r5, #I2C_SR1_ADDR
	bne		Wait_for_the_ADDR_flag_to_be_set;

Clear_Addr:
	ldr		r5, [r4, #SR1];
	ldr		r6, [r4, #SR2];

Clear_ACK:
	ldr 	r3, [r4, #CR1];
	bic 	r3, I2C_CR1_ACK;
	str		r3, [r4, CR1];

Wait_for_BTF_to_be_set:
	ldr  	r5, [r4, #SR1];
	and		r5, #I2C_SR1_BTF
	cmp		r5, #I2C_SR1_BTF
	bne		Wait_for_BTF_to_be_set;

Program_STOP:
	ldr 	r3, [r4, #CR1];
	orr 	r3, I2C_CR1_STOP;
	str		r3, [r4, CR1];

Read_DR_twice:
	ldrb	r2, [r4, #DR]
	ldrb	r1, [r4, #DR]
Form_the_return_value:
	add 	r0, r1, r2, lsl 8;
Disable_I2C2:
	mov		r3, #0;
	str		r3, [r4, #CR1];
End_the_function:
	pop	 	{ 
        r4-r7, lr}
	bx 	 	lr
.size asm_Func_presSen_getVal, .-asm_Func_presSen_getVal

用前文说的方法定义函数asm_Func_presSen_getVal()。进入函数以后先把r4 - r7压入栈里。因为这次个寄存器是被调用函数保护寄存器。剩下的每一段的语句块的含义都跟语句标号一样。最后由于传感器两次传来的值是两个8位的,分别存于r2和r1的低8位。用一句add r0, r1, r2, lsl 8;将传感器的16位值算出并存入r0,也就是返回值。 最后一句.size asm_Func_presSen_getVal, .-asm_Func_presSen_getVal告诉调试器这个函数的大小。

三、运行情况

编译通过以后,从内存分布上检查一下。本函数占104个字节,除了4个压栈,没有额外的内存消耗。

测试线程的C代码如下所示。在tskTest.c里创建了一个测试线程,循环读取传感器的值。

/* * tskTest.c */
#include "tskTest.h"
#include "FreeRTOS.h"
#include "task.h"
#include "stdbool.h"
#include "presSen.h"


static void init(void);

const TskTest_Def tskTest = { 
        
		.init	=	init,
};

#define STACK_SIZE 128
static StaticTask_t TCB_tskTest;
static StackType_t stack_tskTest[STACK_SIZE];
static TaskHandle_t tskTest_handle;
static void tskTest_Entry(void*);

void init(void){ 
        
	tskTest_handle = xTaskCreateStatic(
			tskTest_Entry,
			"tskTest",
			STACK_SIZE,
			(void*)0,
			4,
			stack_tskTest,
			&TCB_tskTest
			);
}

void tskTest_Entry(void* p){ 
        
	static __USED uint16_t sensorVal;
	while(1){ 
        
		sensorVal = presSen.get_SenVal(unit_cmH2O);
		vTaskDelay(pdMS_TO_TICKS(500));
		sensorVal = 0;
	}
}

目前还是把跟这个I2C端口相关的函数封装到一个C结构体下。并且定义了

/* * * presSen.h */
#ifndef PRESSEN_INC_PRESSEN_H_
#define PRESSEN_INC_PRESSEN_H_

#include "stdint.h"

typedef struct _PresSen_Def{ 
        
	void (*init)(void);
	uint16_t (*get_SenVal)(void);

}PresSen_Def;

extern const PresSen_Def presSen;
#endif /* PRESSEN_INC_PRESSEN_H_ */

在C的源程序文件中,暂时先保留init()函数的C语言实现,虽然为空函数。实例化presSen这个块。用extern关键字将汇编函数引进本头文件,并赋给presSen的get_val成员。

/* * presSen.c * * Created on: 2022年6月11日 * Author: SystemUser */

#include "presSen.h"
#include "main.h"
#include "stdbool.h"

/* The address of the sensor is actually 0x28. But the address used in the * program */



static void init(void);

/*Using get_SenVal() to call get_SenVal_unit() to make sure the structure preesSen * is placed in the .text section by the linker.*/
extern uint16_t asm_Func_presSen_getVal(void);

const PresSen_Def presSen = { 
        
	.init	=	init,
	.get_SenVal	=	asm_Func_presSen_getVal,
};

void init(void){ 
        
}

可以看出,每次运行sensorVal都能正确的获得传感器的读数。该驱动程序工作正常。

四、总结

可以看出,用汇编可以严格实现手册上规定的寄存器访问时序,节省内存开支,实现高效的驱动。

标签: 传感器tcbcr1mt交流功率固态继电器cr1u系列特殊型固态继电器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台