一、目标
根据前面对汇编的介绍,我们来实现一个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。
置位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都能正确的获得传感器的读数。该驱动程序工作正常。
四、总结
可以看出,用汇编可以严格实现手册上规定的寄存器访问时序,节省内存开支,实现高效的驱动。