Compare commits

..

11 Commits

Author SHA1 Message Date
7f06fc06fd async driver: fix incorrect timeout handling 2024-03-20 15:35:12 +08:00
67f9e65df8 Start tec_readings_conversion after report is sent
- Before this fix, tec_readings only converts when eth_is_pending
2024-03-20 15:35:12 +08:00
7d2e14ef2f eth: Poll Iface In ETH IRQ
- Improve responsiveness of TCP packets handling
- Fix a bug when one client is connected and active report is ON, the other client's cmds do not receive Ack from Kirdy
2024-03-20 15:32:28 +08:00
e525a3f354 fix typo 2024-03-20 12:06:05 +08:00
8431e9f43d Update README 2024-03-20 11:55:21 +08:00
e9d5f4598a Fix wrong USB Product Description 2024-03-20 11:45:09 +08:00
1867935047 Send report to connected socket
- Remove active report in flash settings
2024-03-19 17:38:29 +08:00
99cf17f7e4 temp_mon: Add support for constant current mode 2024-03-19 14:52:26 +08:00
048245f674 set_tec_max_i_pos/neg->...cooling_i/heating_i 2024-03-19 14:52:26 +08:00
0a01d299bc Fix wrong reporting of Thermostat Pwr_on status 2024-03-19 14:52:26 +08:00
dbbd438e92 ld: Fix unable to save & load ld_i flash settings 2024-03-19 14:48:25 +08:00
13 changed files with 154 additions and 94 deletions

View File

@ -41,7 +41,7 @@ gdb target/thumbv7em-none-eabihf/release/kirdy
```
## Flashing
There are several options for flashing kirdy. DFU requires only a USB-C cable or RJ45 cable, whereas OpenOCD needs a JTAG/SWD adapter.
If the firmware to be flashed involves an update on the flash settings, it is required to erase the flash settings before flashing the new firmware to avoid unexpected hardware behavior. There are several options for flashing kirdy. DFU requires only a USB-C cable or RJ45 cable, whereas OpenOCD needs a JTAG/SWD adapter.
### dfu-util on Linux
* Install the DFU USB tool (dfu-util).
@ -64,7 +64,12 @@ On a Windows machine install [st.com](https://st.com) DfuSe USB device firmware
- remove jumper
- cycle power to leave DFU update mode
### Erasing Flash Settings
### OpenOCD
```shell
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program target/thumbv7em-none-eabihf/debug/kirdy verify reset; exit"
```
## Erasing Flash Settings
The flash settings are stored in the last flash sector(ID: 11) of bank 0 of stm32f405. You can erase it with JTAG/SWD adapter or by putting the device in Dfu mode. You may find it useful if you have set network settings incorrectly.
With JTAG/SWD adapter connected, issue the following command:
@ -78,8 +83,3 @@ With STM32 in DFU Mode, connect the USB Type C cable and then issue the followin
```
dfu-util -a 0 -s 0x080E0000:leave -D erase_flash_settings.bin
```
### OpenOCD
```shell
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program target/thumbv7em-none-eabihf/debug/kirdy verify reset; exit"
```

View File

@ -358,16 +358,16 @@ class Thermostat:
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetTecMaxV", max_v)
async def set_tec_max_i_pos(self, max_i_pos):
async def set_tec_max_cooling_i(self, max_i_pos):
"""
Set Tec maximum positive output
Set Tec maximum cooling current (Settable Range: 0.0 - 1.0)
- max_i_pos: A
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetTecMaxIPos", max_i_pos)
async def set_tec_max_i_neg(self, max_i_neg):
async def set_tec_max_heating_i(self, max_i_neg):
"""
Set Tec maximum negative output
Set Tec maximum heating current (Settable Range: 0.0 - 1.0)
- max_i_neg: A
"""
return await self._send_cmd(TARGET_THERMOSTAT, "SetTecMaxINeg", max_i_neg)
@ -609,7 +609,7 @@ class Kirdy:
line = await asyncio.shield(self._read_write(cmd))
async def end_session(self):
"""End session to Thermostat if connected, cancel connection if connecting"""
"""End session to Kirdy if connected, cancel connection if connecting"""
if self._connecting_task is not None:
self._connecting_task.cancel()
@ -625,7 +625,7 @@ class Kirdy:
async def _read_response(self):
try:
response = await asyncio.wait_for(self._reader.read(1024), self.timeout)
except TimeoutError:
except asyncio.exceptions.TimeoutError:
return {
"msg_type": "Internal Timeout"
}
@ -645,12 +645,10 @@ class Kirdy:
self._writer.write(bytes(json.dumps(cmd), "UTF-8"))
await self._writer.drain()
response = await self._read_response()
try:
if response["msg_type"] == "Acknowledge":
return response
except:
retry += 1
await asyncio.sleep(0.25)
await asyncio.sleep(0.1)
raise NoAckRecv
async def _send_cmd_handler(self, target, cmd, data=None):
@ -681,10 +679,8 @@ class Kirdy:
self._writer.write(bytes(json.dumps(cmd_dict), "UTF-8"))
await self._writer.drain()
response = await self._read_response()
try:
if response["msg_type"] == "Acknowledge":
return response
except:
retry += 1
await asyncio.sleep(0.1)
raise NoAckRecv

View File

@ -278,8 +278,8 @@ async def main():
await kirdy.thermostat.set_temp_mon_upper_limit(target_temperature + 20)
await kirdy.thermostat.set_temp_mon_lower_limit(target_temperature - 20)
await kirdy.thermostat.set_tec_max_i_pos(output_step)
await kirdy.thermostat.set_tec_max_i_neg(output_step)
await kirdy.thermostat.set_tec_max_cooling_i(output_step)
await kirdy.thermostat.set_tec_max_heating_i(output_step)
# The Polling Rate of Temperature Adc is equal to the PID Update Interval
await kirdy.thermostat.config_temp_adc_filter("Sinc5Sinc1With50hz60HzRejection", "F16SPS")

View File

@ -33,7 +33,7 @@ impl State {
let serial = SerialPort::new(bus);
let dev = UsbDeviceBuilder::new(bus, UsbVidPid(0x16c0, 0x27dd))
.manufacturer("M-Labs")
.product("thermostat")
.product("Kirdy")
.device_release(0x20)
.self_powered(true)
.device_class(usbd_serial::USB_CLASS_CDC)

View File

@ -118,7 +118,7 @@ impl LdDrive{
}
pub fn power_up(&mut self){
let prev_i_set = LdCurrentOutCtrlTimer::get_target_i();
let prev_i_set = self.settings.ld_drive_current;
LdCurrentOutCtrlTimer::reset();
let _ = self.ctrl.set_i(ElectricCurrent::new::<milliampere>(0.0), Settings::LD_DRIVE_TRANSIMPEDANCE, Settings::DAC_OUT_V_MAX);
LdPwrExcProtector::pwr_on_and_arm_protection();
@ -142,14 +142,14 @@ impl LdDrive{
}
pub fn ld_set_i(&mut self, i: ElectricCurrent){
LdCurrentOutCtrlTimer::set_target_i_and_listen_irq(i, self.settings.ld_drive_current);
self.settings.ld_drive_current = i;
LdCurrentOutCtrlTimer::set_target_i_and_listen_irq(i, self.ctrl.get_i_set());
}
pub fn poll_and_update_output_current(&mut self) -> ElectricCurrent {
match LdCurrentOutCtrlTimer::get_irq_status() {
Some(i_set) => {
let i_set = self.ctrl.set_i(i_set, Settings::LD_DRIVE_TRANSIMPEDANCE, Settings::DAC_OUT_V_MAX);
self.settings.ld_drive_current = i_set;
LdCurrentOutCtrlTimer::clear_alarm_and_resume_listening();
i_set
}
@ -195,10 +195,15 @@ impl LdDrive{
}
pub fn get_status_report(&mut self) -> StatusReport {
let ld_i_set = if LdPwrExcProtector::get_status().pwr_engaged {
self.ctrl.get_i_set()
} else {
ElectricCurrent::new::<ampere>(0.0)
};
StatusReport {
pwr_on: LdPwrExcProtector::get_status().pwr_engaged,
pwr_excursion: LdPwrExcProtector::get_status().pwr_excursion,
ld_i_set: self.settings.ld_drive_current,
ld_i_set: ld_i_set,
pd_i: self.get_pd_i(),
pd_pwr: self.get_pd_pwr(),
term_status: self.get_term_status(),

View File

@ -9,6 +9,7 @@ use stm32f4xx_hal::{
use uom::si::{
ratio::ratio,
f32::{ElectricPotential, ElectricCurrent},
electric_current::ampere,
};
use crate::laser_diode::max5719::{self, Dac};
@ -49,12 +50,14 @@ type DacLoad = PB14<Output<PushPull>>;
pub struct LdCtrl{
pub phy: LdCtrlPhy<Channel0>,
i_set: ElectricCurrent,
}
impl LdCtrl {
pub fn new(phy_ch0: LdCtrlPhy<Channel0>) -> Self {
LdCtrl {
phy: phy_ch0,
i_set: ElectricCurrent::new::<ampere>(0.0)
}
}
@ -85,6 +88,11 @@ impl LdCtrl {
}
pub fn set_i(&mut self, current: ElectricCurrent, transimpedance: TransimpedanceUnit, dac_out_v_max: ElectricPotential) -> ElectricCurrent {
self.set_dac(current * transimpedance, dac_out_v_max) / transimpedance
self.i_set = self.set_dac(current * transimpedance, dac_out_v_max) / transimpedance;
self.i_set
}
pub fn get_i_set(&mut self) -> ElectricCurrent{
self.i_set
}
}

View File

@ -63,13 +63,6 @@ impl LdCurrentOutCtrlTimer {
}
}
pub fn get_target_i() -> ElectricCurrent {
if let Some(ref mut ld_current_out_ctrl_timer ) = LdCurrentOutCtrlTimer::get() {
return ld_current_out_ctrl_timer.target_i
}
ElectricCurrent::new::<ampere>(0.0)
}
pub fn set_alarm() {
if let Some(ref mut ld_current_out_ctrl_timer ) = LdCurrentOutCtrlTimer::get() {
ld_current_out_ctrl_timer.timeout = true;

View File

@ -31,7 +31,6 @@ static mut ETH_DATA_BUFFER: [u8; 1024] = [0; 1024];
#[derive(Deserialize, Serialize, Clone, Copy, Debug)]
pub struct DeviceSettings{
report_readings: bool,
ip_settings: IpSettings,
}
@ -59,10 +58,11 @@ fn main() -> ! {
let (mut wd, mut flash_store, mut laser, mut thermostat,) = bootup(core_perif, perif);
let mut device_settings = DeviceSettings {
report_readings: false,
ip_settings: IpSettings::default()
};
let mut active_report: [bool; net::net::NUM_OF_SOCKETS] = [false; net::net::NUM_OF_SOCKETS];
let mut state = State::default();
loop {
@ -133,12 +133,10 @@ fn main() -> ! {
}
State::MainLoop => {
let mut eth_is_pending = false;
let mut has_temp_reading = false;
laser.poll_and_update_output_current();
if thermostat.poll_adc() {
has_temp_reading = true;
thermostat.update_pid();
if thermostat.get_temp_mon_status().over_temp_alarm {
laser.power_down();
@ -146,43 +144,42 @@ fn main() -> ! {
thermostat.power_down();
}
net::net::for_each(|mut socket| {
if net::net::eth_is_socket_active(socket) {
if device_settings.report_readings {
net::net::for_each(|mut socket, id| {
if net::net::eth_is_socket_active(socket) && net::net::eth_is_socket_connected(socket) {
if active_report[id] {
unsafe {
net::cmd_handler::send_status_report(&mut ETH_DATA_BUFFER, &mut laser, &mut thermostat, &mut socket);
}
}
}
});
else {
active_report[id] = false;
}
});
thermostat.start_tec_readings_conversion();
}
net::net::for_each(|mut socket| {
if net::net::eth_is_socket_active(socket) {
cortex_m::interrupt::free(|cs|
{
eth_is_pending = net::net::is_pending(cs);
net::net::clear_pending(cs);
}
);
if eth_is_pending {
net::net::for_each(|mut socket, id| {
if net::net::eth_is_socket_active(socket) && net::net::eth_is_socket_connected(socket){
unsafe{
cortex_m::interrupt::free(|cs| {
net::net::clear_pending(cs);
});
let bytes = net::net::eth_recv(&mut ETH_DATA_BUFFER, socket);
if bytes != 0 {
info!("Ts: {:?}", sys_timer::now());
debug!("Number of bytes recv: {:?}", bytes);
// State Transition
net::cmd_handler::execute_cmd(&mut ETH_DATA_BUFFER, bytes, &mut socket, &mut laser, &mut thermostat, &mut state, &mut device_settings);
net::cmd_handler::execute_cmd(&mut ETH_DATA_BUFFER, bytes, &mut socket, &mut laser, &mut thermostat, &mut state, &mut device_settings, &mut active_report[id]);
}
}
}
if has_temp_reading {
thermostat.start_tec_readings_conversion();
}
}
});
})
};
}
State::SaveFlashSettings => {
// State Transition
@ -226,7 +223,7 @@ fn main() -> ! {
wd.feed();
laser.power_down();
thermostat.power_down();
net::net::for_each(|mut socket| {
net::net::for_each(|mut socket, _| {
if net::net::eth_is_socket_active(socket) {
unsafe {
net::cmd_handler::send_response(&mut ETH_DATA_BUFFER, net::cmd_handler::ResponseEnum::HardReset, None, &mut socket);
@ -239,7 +236,7 @@ fn main() -> ! {
laser.power_down();
thermostat.power_down();
let mut any_socket_alive = false;
net::net::for_each(|socket| {
net::net::for_each(|socket, _| {
if net::net::eth_is_socket_active(socket) {
net::net::eth_close_socket(socket);
any_socket_alive = true;

View File

@ -221,7 +221,7 @@ pub struct TecSetICmd {
/// Make sure kirdy's firmware is flashed with release builds.
/// The received message must contain only one json cmd. TCP client should set TCP_NODELAY or equivalent flag in its TCP Socket
/// Settings to avoid unwanted buffering on TX Data and minimize TX latency.
pub fn execute_cmd(buffer: &mut [u8], buffer_size: usize, socket: &mut SocketHandle, laser: &mut LdDrive, thermostat: &mut Thermostat, state: &mut State, device_settings: &mut DeviceSettings){
pub fn execute_cmd(buffer: &mut [u8], buffer_size: usize, socket: &mut SocketHandle, laser: &mut LdDrive, thermostat: &mut Thermostat, state: &mut State, device_settings: &mut DeviceSettings, active_report: &mut bool){
let mut cmd = TecSetICmd {
json: TecSetICmdJson::default()
};
@ -266,7 +266,7 @@ pub fn execute_cmd(buffer: &mut [u8], buffer_size: usize, socket: &mut SocketHan
match cmd.json.data_bool{
Some(val) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket);
device_settings.report_readings = val;
*active_report = val;
}
None => {
send_response(buffer, ResponseEnum::InvalidDatatype, Some(ERR_MSG_MISSING_DATA_BOOL), socket);

View File

@ -61,7 +61,7 @@ pub struct EthernetMgmtPins {
}
pub type EthInterface = Interface;
const NUM_OF_SOCKETS : usize = 4;
pub const NUM_OF_SOCKETS : usize = 4;
const TCP_BUFFER_SIZE: usize = 2048;
static mut RX_RING: Option<[RxRingEntry; 8]> = None;
static mut TX_RING: Option<[TxRingEntry; 2]> = None;
@ -207,8 +207,11 @@ impl ServerHandle {
self.link_was_up = self.phy.phy_link_up();
}
pub fn recv(&mut self, buffer: &mut [u8], socket_handles: SocketHandle)-> Result<usize, smoltcp::socket::tcp::RecvError> {
pub fn poll_iface(&mut self) {
self.iface.poll(now_fn(), &mut &mut self.dma, &mut self.socket_set);
}
pub fn recv(&mut self, buffer: &mut [u8], socket_handles: SocketHandle)-> Result<usize, smoltcp::socket::tcp::RecvError> {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
socket.recv_slice(buffer)
@ -218,11 +221,14 @@ impl ServerHandle {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
if num_bytes > 0 {
socket.send_slice(&buffer[..num_bytes]).ok();
self.poll_iface();
info!("Sent {} bytes.", num_bytes);
}
}
// Send bytes out
self.iface.poll(now_fn(), &mut &mut self.dma, &mut self.socket_set);
pub fn is_socket_connected(&mut self, socket_handles: SocketHandle)->bool {
let socket = self.socket_set.get_mut::<Socket>(socket_handles);
socket.state() == State::Established
}
pub fn poll_socket_status(&mut self, socket_handles: SocketHandle)-> bool {
@ -357,6 +363,17 @@ pub fn eth_poll_and_update_link_speed() {
}
}
pub fn eth_poll_iface() {
unsafe {
if let Some(ref mut server_handle ) = SERVER_HANDLE {
server_handle.poll_iface();
}
else {
panic!("eth_poll_packet is called before init");
}
}
}
pub fn eth_send(buffer: &mut [u8], num_bytes: usize, socket_handles: SocketHandle) {
unsafe {
if let Some(ref mut server_handle ) = SERVER_HANDLE {
@ -385,6 +402,17 @@ pub fn eth_recv(buffer: &mut [u8], socket_handles: SocketHandle)-> usize{
}
}
pub fn eth_is_socket_connected(socket_handles: SocketHandle) -> bool {
unsafe {
if let Some(ref mut server_handle ) = SERVER_HANDLE {
server_handle.is_socket_connected(socket_handles)
}
else {
panic!("eth_is_socket_connected is called before init");
}
}
}
pub fn eth_is_socket_active(socket_handles: SocketHandle) -> bool {
unsafe {
if let Some(ref mut server_handle ) = SERVER_HANDLE {
@ -407,11 +435,11 @@ pub fn eth_close_socket(socket_handles: SocketHandle) {
}
}
pub fn for_each<F: FnMut(SocketHandle)>(mut callback: F) {
pub fn for_each<F: FnMut(SocketHandle, usize)>(mut callback: F) {
unsafe {
if let Some(ref mut server_handle ) = SERVER_HANDLE {
for i in 0..NUM_OF_SOCKETS {
callback(server_handle.socket_handles[i]);
callback(server_handle.socket_handles[i], i);
}
}
else {
@ -426,10 +454,12 @@ pub fn for_each<F: FnMut(SocketHandle)>(mut callback: F) {
fn ETH() {
let interrupt_reason = stm32_eth::eth_interrupt_handler();
cortex_m::interrupt::free(|cs| {
if interrupt_reason.rx {
*NET_PENDING.borrow(cs)
.borrow_mut() = true;
eth_poll_iface();
}
});
debug!("Ethernet Interrupt{:?}", interrupt_reason);
}

View File

@ -218,7 +218,7 @@ impl MAX1968 {
}
pub fn is_powered_on(&mut self) -> bool {
self.phy.shdn.is_set_low()
self.phy.shdn.is_set_high()
}
pub fn power_down(&mut self) {

View File

@ -11,6 +11,7 @@ pub enum TempStatusEnum {
OverTemp,
Unstable,
Stable,
ConstantCurrentMode,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
@ -56,7 +57,8 @@ impl Default for TempMon {
#[derive(Default, PartialEq, Debug)]
enum State {
#[default]
PidOff,
PwrOff,
ConstantCurrentMode,
PidStartUp,
PidStable,
OverTempAlarm,
@ -95,16 +97,38 @@ impl TempMon {
self.state = State::default();
}
pub fn update_status(&mut self, pid_engaged: bool, temp: ThermodynamicTemperature) {
pub fn update_status(&mut self, pid_engaged: bool, pwr_on: bool, temp: ThermodynamicTemperature) {
match self.state {
State::PidOff => {
State::PwrOff => {
self.is_set_point_changed = false;
self.status.status = TempStatusEnum::Off;
self.count = 0;
// State Transition
if pwr_on {
if pid_engaged {
self.state = State::PidStartUp;
} else {
self.state = State::ConstantCurrentMode
}
}
}
State::ConstantCurrentMode => {
let is_over_temp = temp > self.upper_limit || temp < self.lower_limit;
self.status.status = TempStatusEnum::ConstantCurrentMode;
if is_over_temp {
self.state = State::OverTempAlarm;
self.status.status = TempStatusEnum::OverTemp;
}
else if !pwr_on {
self.state = State::PwrOff;
self.status.status = TempStatusEnum::Off;
} else if pid_engaged {
self.state = State::PidStartUp;
self.status.status = TempStatusEnum::Unstable;
self.is_set_point_changed = false;
}
}
State::PidStartUp | State::PidStable => {
@ -134,6 +158,10 @@ impl TempMon {
}
// State Transition
if !pwr_on {
self.state = State::PwrOff;
}
else {
if self.status.status == TempStatusEnum::OverTemp {
self.state = State::OverTempAlarm;
} else if self.is_set_point_changed {
@ -141,6 +169,9 @@ impl TempMon {
self.state = State::PidStartUp;
} else if self.status.status == TempStatusEnum::Stable {
self.state = State::PidStable;
} else if !pid_engaged {
self.state = State::ConstantCurrentMode
}
}
}
State::OverTempAlarm => {

View File

@ -186,7 +186,7 @@ impl Thermostat{
state.update(data);
let pid_engaged = state.get_pid_engaged();
let temp = self.get_temperature();
self.temp_mon.update_status(pid_engaged, temp);
self.temp_mon.update_status(pid_engaged, self.max1968.is_powered_on(), temp);
debug!("state.get_pid_engaged(): {:?}", pid_engaged);
debug!("Temperature: {:?} degree", temp.get::<degree_celsius>());
data_rdy = true;