GPT4 as a Hardware Abstraction Layer
Hardware Abstraction Layers (HALs) are software components that provide a uniform interface to interact with hardware, abstracting away the underlying complexities. REST libraries, on the other hand, simplify communication with RESTful APIs, providing convenient methods to make requests and handle responses.
Pros & Cons of HALs & REST Libraries #
Benefits #
HALs and REST libraries are essential tools that speed up development, especially in an age before GPT-4. They allow developers to focus on writing application logic without having to worry about low-level hardware details or the intricacies of API communication.
Limitations #
Despite their benefits, HALs and REST libraries can also limit development:
- HALs:
- Abstract away hardware, potentially preventing you from performing specific tasks.
- May suffer from poor documentation or maintenance.
- Can cause unexpected behavior and make debugging difficult.
- (Thicc) REST libraries:
- Abstract away the API, potentially preventing you from performing specific tasks.
- May suffer from poor maintenance for your chosen language.
REST APIs which are instead one-to-one mappings of the API’s endpoints can be considered “thin” REST libraries and do not suffer from the same limitations as thick REST libraries. Generally, the one-to-one mapping of the HAL is instead considered a “Peripheral Access Crate” (PAC).
Peripheral Access Crates (PACs) #
The PAC is a one-to-one mapping from a microcontroller’s registers to code.
An example of a PAC is the stm32h7
library, which is a Peripheral Access Crate (PAC) for the STM32 family of
microcontrollers. PACs provide a thin wrapper around hardware, offering a more direct way to interact with it.
Example: Using a PAC to Blink an LED #
#[entry]
fn main() -> ! {
let peripherals = stm32h7x3::Peripherals::take().unwrap();
let pwr = &peripherals.PWR;
let rcc = &peripherals.RCC;
// Enable the GPIO clock (using the PAC)
rcc.ahb4enr.modify(|_, w| w.gpioden().set_bit());
// Enable voltage scaling (using the PAC)
pwr.cr3.modify(|_, w| w.svden().enabled());
// Configure GPIO pin D15 as output for the LED (using the PAC)
let gpiod = &peripherals.GPIOD;
gpiod.moder.modify(|_, w| w.moder15().output());
loop {
// Toggle the LED (using the PAC)
gpiod.odr.modify(|r, w| w.odr15().bit(!r.odr15().bit()));
// Delay (not precise)
asm::delay(8_000_000);
}
}
GPT-4 for thin REST and PAC Calls #
GPT-4 makes it much easier to implement logic that combines multiple REST or PAC calls. While reading documentation to combine these calls is not too difficult, it can be time-consuming. Using GPT-4, you can feed in the REST API/reference manual docs and obtain code that works for your specific use case, saving a considerable amount of time. Since the task of transferring from the style of an english doc to code is not creating new knowledge—but just style transferring—GPT-4 generally performs quite well. Even without docs, GPT-4 usually still generates code that mostly works—albeit with a few modifications.
Using an svd
file to generate a PAC #
An svd
file is a standardized XML format for describing the registers of a microcontroller. It is used by PACs to
generate code for interacting with the microcontroller’s registers. A portion of the svd
file for the COMP1 peripheral
is listed below:
<peripheral>
<name>COMP1</name>
<description>COMP1</description>
<groupName>COMP1</groupName>
<!-- ...-->
<registers>
<!-- ... -->
<register>
<name>CFGR1</name>
<displayName>CFGR1</displayName>
<description>Comparator configuration register 1</description>
<addressOffset>0xC</addressOffset>
<size>0x20</size>
<access>read-write</access>
<resetValue>0x00000000</resetValue>
<fields>
<!-- ... -->
<field>
<name>POLARITY</name>
<description>COMP channel 1 polarity selection
bit
</description>
<bitOffset>3</bitOffset>
<bitWidth>1</bitWidth>
</field>
<!-- ... -->
</fields>
</register>
<!-- ... -->
</registers>
</peripheral>
The full SVD containing COMP1 can be downloaded here: comp1.svd. The full STM32H743 SVD (which contains all peripherals) can be downloaded here: STM32H743.svd.
Concatenating the full comp1.svd
with the prompt:
in Rust using the stm32h7 crate modify registers of COMP1 to negate the output of the comparison
and pasting into ChatGPT(4) yields the code:
// Add this to your Cargo.toml
// [dependencies]
// stm32h7 = "0.13.0"
use stm32h7::stm32h7x3;
fn main() {
// Get access to the device specific peripherals
let peripherals = stm32h7x3::Peripherals::take().unwrap();
// Configure the COMP1 peripheral
configure_comp1(&peripherals);
}
fn configure_comp1(peripherals: &stm32h7x3::Peripherals) {
// Get access to the COMP1 peripheral
let comp1 = &peripherals.COMP1;
// Modify the CFGR1 register to negate the output of the comparison
comp1.cfgr1.modify(|_, w| w.polarity().set_bit());
// Add any other configuration as needed
}
which is correct.
Without an svd
#
we obtain
comp1.csr.modify( | _, w| w.neg().set_bit());
which is incorrect code (does not compile and uses wrong registers). This shows that GPT4 understands the
semantics of how registers are set with the stm32h7
crate, but does not necessarily know what the registers
are without extra context. We are using GPT4 to transform the [english prompt + SVD] -> [Rust code]
using the
style of the stm32h7
crate. I have seen GPT4 generate exceptional outputs when all it needs to do is
style transfer and is not generating “new” knowledge.
Asking for TIM15 (trimmed svd) #
I had to trim the indentation and removed bit-offset tags so the SVD file could fit in 8k tokens required by GPT-4.
I do not yet have access to gpt-4-32k
which can handle up to 32k
tokens. The prompt:
In Rust using the stm32h7 crate modify registers of TIM15 (OC1) to output a clock duty 50%. Show how to set the pre-scalar as well. Include comments
We obtain
use stm32h7xx_hal::{
pac::TIM15,
prelude::*,
};
fn config_tim15(tim15: &TIM15, prescaler: u16) {
// Set the prescaler
tim15.psc.write(|w| w.psc().bits(prescaler));
// Set timer to auto-reload mode and enable auto-reload preload
tim15.cr1.modify(|_, w| {
w.opm().continuous()
.arpe().enabled()
});
// Set CCR1 value to half for 50% duty cycle
let arr = tim15.arr.read().arr().bits();
tim15.ccr1.write(|w| w.ccr1().bits(arr / 2));
// Set output compare mode
tim15.ccmr1_output.modify(|_, w| {
w.oc1m().pwm_mode1()
.oc1pe().enabled()
});
// Enable capture/compare output for channel 1
tim15.ccer.modify(|_, w| w.cc1e().set_bit());
// Enable the timer
tim15.cr1.modify(|_, w| w.cen().enabled());
}
This is mostly correct and can be modified to get:
/// Create a new blanking timer and init clock
fn main() {
// ..
// Set the prescaler
tim15.psc.write(|w| w.psc().bits(TIM15_PRESCALER));
// Set timer to auto-reload mode and enable auto-reload preload
tim15.cr1.modify(|_, w| {
w.opm()
.variant(OPM_A::Disabled)
.arpe()
.variant(ARPE_A::Enabled)
});
// Set CCR1 value to half for 50% duty cycle
let arr = tim15.arr.read().arr().bits();
tim15.ccr1().write(|w| w.ccr().bits(arr / 2));
// Set output compare mode
tim15.ccmr1_output()
.modify(|_, w| w.oc1m().variant(PwmMode1).oc1pe().variant(OC1PE_A::Enabled));
// Enable capture/compare output for channel 1
tim15.ccer.modify(|_, w| w.cc1e().set_bit());
// Enable the timer
tim15.cr1.modify(|_, w| w.cen().enabled());
}
Asking questions #
With the same SVD we can ask
What is TIM15 OC1. Is OC1 a channel or output source or something? What does it stand for?
and we get:
TIM15 is a general-purpose timer that can be used for various timing and control tasks in a microcontroller system. OC1 (Output Compare 1) is a channel and is an important feature of the TIM15 timer. The acronym “OC” stands for Output Compare, which refers to the ability of the timer to compare the counter value with a predefined value stored in a register (CCR1 in this case).
When the counter value matches the value stored in the CCR1 register, an event is generated, which can trigger an interrupt, create a pulse on an output pin, or perform some other related action. This functionality can be used for generating periodic signals, controlling PWM (Pulse Width Modulation) outputs, and implementing other timing-related tasks in the system.
In summary, OC1 in TIM15 is a channel used for comparing the counter value with a predefined value to trigger specific actions, and it stands for Output Compare 1.
Rust: A Perfect Match for Language Models #
Rust is an excellent choice for working with Language Models like GPT-4 for several reasons:
-
Token efficiency: Rust has a low token count, which is beneficial since you pay for the number of tokens generated by the Language Model.
- With
serde
, you can use#[derive(Deserialize)]
and#[derive(Serialize)]
for a struct, then useserde_json::from_str
andserde_json::to_string
to convert between JSON and Rust structs without generating extra code.
- With
-
Code verification: Rust makes it easier to verify the correctness of the generated code.
- PACs in Rust use
enum
s to represent different registers, ensuring type correctness at compile-time. - Rust’s strong typing makes it harder for mistakes to go unnoticed, which is crucial when working with Language Models, as they can occasionally generate incorrect code.
- PACs in Rust use
-
Clarity: Rust’s syntax and constructs, like
serde
, make it clear what’s happening in the code, which is important for developers who need to understand and maintain it.
Conclusion #
By combining the power of Rust with GPT-4, you can create efficient, verifiable, and easily understandable code for your hardware abstraction and REST API needs. This approach allows you to bypass the limitations of traditional HALs and REST libraries, ultimately leading to more streamlined and effective development processes.